← Table of Contents
Chapter 11

Developer Reference

APIs for agent authors, player implementors, and character consumers.

This chapter is for developers. It covers three distinct roles in the Character Studio ecosystem and the API contracts each one depends on. If you are writing an agent to extend a character, you want Part 1. If you are building a player — a library or application that runs Character Studio characters — you want Part 2. If you are embedding a character into a website or application using an existing player, you want Part 3.

Character Studio itself currently implements all three roles in a single application: it is both the editing environment and the player. This chapter is written for the future state in which these roles are separated — where characters are portable packages that run in standalone players. The APIs described here are the contracts that make that portability possible.

Design principle

These APIs are defined by their behavior contracts, not by any specific implementation. A player built in React, a player built in Swift, and a server-side player built in Python can all be conformant implementations of the bridge and player APIs described here — as long as they honor the contracts, characters and agents will work in all of them.

Architecture Overview

Three layers work together to run a character:

┌─────────────────────────────────────────────┐
│  Consumer  (website, app, or Studio itself) │
│  Calls the Player API to send messages      │
│  and receive responses                      │
└────────────────────┬────────────────────────┘
                     │ Player API
┌────────────────────▼────────────────────────┐
│  Player  (the runner)                       │
│  Manages the conversation, calls the LLM,   │
│  brokers tool calls, runs agents            │
└────────────────────┬────────────────────────┘
                     │ Bridge API
┌────────────────────▼────────────────────────┐
│  Agents  (JS modules in the character pkg)  │
│  Implement tools the model can call,        │
│  maintain persistent state per conversation │
└─────────────────────────────────────────────┘

A character package is the portable artifact that moves between these layers. It contains everything the player needs to run a character: the text files, the configuration, and all the agent modules the character depends on. Packages are self-contained — the player does not need to know which specific agents a character uses in advance.

The Character Package

A character package is a directory (or archive) with the following structure. File paths shown are defaults — all character content files and the context directory can be renamed by the character author via fileMappings in config.json. Players must resolve paths through fileMappings before reading any file. See the fileMappings section below.

my-character/
  config.json                 ← Character configuration (never remapped)
  instructions.txt            ← Character definition       (default name)
  system-prompt.txt           ← System prompt template     (default name)
  description.txt             ← Short description          (default name)
  intro.txt                   ← Opening message            (default name)
  context/                    ← Additional context files   (default directory name)
    *.txt
  agents/                     ← Agent modules — path is fixed, not remappable
    namespace.agent-name.js
    ...
    lib/                      ← Pre-built JS libraries declared with @lib
      library-name.js
      ...
  namespace.agent-name.agent.json   ← Default agent state (optional, per agent)

config.json

The character configuration file. Relevant fields:

{
  "name": "Aria",
  "model": "model-id",
  "agents": [
    "studio.memory-store",
    "my-character.battle-manager"
  ],
  "fileMappings": {
    "Instructions":    "eve-character-sheet.txt",
    "System Prompt":   "prompts/system.txt",
    "Description":     "pub/description.txt",
    "Introduction":    "pub/intro.txt",
    "ContextDirectory": "world-lore/"
  }
}

The "agents" array lists agent IDs in load order. The player loads each agent from the agents/ directory within the package as {agentId}.js. Load order is registration priority: the first agent to register a tool name or command name wins; later agents are silently skipped for any collision.

fileMappings — required player support

Character authors can rename any of the standard content files and the context directory. A conformant player must apply these mappings before reading any character file. Assuming default file names is not valid.

Security: path traversal

All paths in fileMappings must be treated as relative to the character package root — the directory containing config.json. Players must reject or sanitize any path that attempts to escape the package, specifically paths containing .. or beginning with /. In web-based players using FSA or OPFS, the browser's file system APIs enforce this automatically. Standalone and server-side players are responsible for their own enforcement.

KeyDefault pathWhat it points to
"Instructions"instructions.txtCharacter definition file
"System Prompt"system-prompt.txtSystem prompt template
"Description"description.txtShort character description
"Introduction"intro.txtOpening message shown before first turn
"ContextDirectory"context/Directory containing context files

Keys not present in fileMappings retain their defaults. config.json itself and the agents/ directory are never remapped — their paths are always fixed.

The recommended player implementation is a path resolver built immediately after loading config.json:

const DEFAULTS = {
    "Instructions":     "instructions.txt",
    "System Prompt":    "system-prompt.txt",
    "Description":      "description.txt",
    "Introduction":     "intro.txt",
    "ContextDirectory": "context",
};

function resolvePath(key, config) {
    return config.fileMappings?.[key] ?? DEFAULTS[key];
}

// Usage:
const instructionsPath = resolvePath("Instructions", config);
const contextDir       = resolvePath("ContextDirectory", config);

All subsequent file reads go through this resolver. A player that hard-codes instructions.txt will silently serve wrong content for any character that has renamed its files — which is a source of confusing, hard-to-diagnose failures.

Bundling built-in agents

When a character is exported for distribution, any built-in platform agents it uses must be copied into the package's agents/ directory. This makes the package self-contained: the player does not need to know about any specific built-in agents, and version mismatches between the authoring tool and the player cannot affect the character's behavior. The player simply loads whatever agent files are present in the package.

Default agent state

If a file named {agentId}.agent.json exists in the package root, the player uses it as the initial state for that agent in any new conversation. This allows a character to ship with pre-configured starting data — initial memories, starting stats, a pre-populated NPC roster. Existing conversations are never affected when this file changes.

System prompt macros

The system prompt template supports one macro that the player expands before sending to the model:

Character context is appended after the expanded system prompt as a single block. The recommended (but not required) method of including the context is to write a simple prefix of "The following information is provided as background context for this character. It is not always relevant. Only refer to it if it's relevant to the discussion:". Each context file should be separated by two carriage returns. The entire context block should be sent with the role: "user" and isFile: true.

The Agent preload context (see onLoad below) is appended after the character context block and should also be sent with the role: "user" and isFile: true.

Part 1 — Agent Authors

An agent is a JavaScript, TypeScript, Python, or Lua module that extends a character with persistent state and model-callable tools. The four languages are full peers — one contract, one bridge API. This section covers the contract in its JS form and notes the Python and Lua differences where they exist.

The manifest

Every agent file begins with a structured block that declares its capabilities. The player parses this block before executing any code. In JS/TS it is a /** ... */ comment; in Python it is the module docstring with the identical tag vocabulary (tag lines need no leading *); in Lua it is a top-of-file --[[ ... ]] block comment with the same tags (plain --[[ ]] only — long-bracket forms are not recognized):

"""
@agent namespace.agent-name
@description Human-readable description shown in player UIs
@tool tool_name
@preload true
"""
/**
 * @agent namespace.agent-name
 * @version 1.0
 * @description Human-readable description shown in player UIs
 *
 * @tool tool_name
 * @tool-description What the model should call this tool for
 * @tool-params {"type":"object","properties":{...},"required":[...]}
 *
 * @command @cmd handlerExport
 *
 * @preload true
 * @api studio.history
 * @lib jmespath
 * @debug true
 */
FieldRequiredDescription
@agentYesUnique agent ID. Use a namespace prefix that identifies you: mysite.battle-manager. The studio. prefix is reserved for platform built-ins.
@versionNoSemver string. Informational.
@descriptionNoShown in player UIs and store panels.
@tool0+Declares a tool the model can call. Each @tool block spans one @tool, one @tool-description, and one @tool-params.
@tool-descriptionNoOne sentence describing when the model should call this tool. Sent directly to the model in the tool definition.
@tool-paramsNoJSON Schema object describing the tool's parameters.
@command0+Declares a direct command and the exported function that handles it. Format: @command @name exportedFn.
@preloadNotrue to call onLoad at conversation start. Default: false.
@api0+Declares which bridge API namespaces this agent uses. Currently informational; will become a permission gate when agents run in sandboxed workers.
@lib0+Declares a pre-built JavaScript library dependency from agents/lib/. Each @lib name resolves to agents/lib/name.js (falling back to name.min.js). The library is evaluated in the agent's execution context before the agent module loads, registering itself on the global scope. Only IIFE/UMD format libraries are supported — ES modules are not. JavaScript/TypeScript agents only: a Python or Lua agent declaring @lib gets a load warning and no libraries. See Library Files below.
@debugNotrue to enable per-conversation activity logging for this agent. When active, Studio writes a timestamped record of every tool call and direct command (args, result, duration) to a sidecar log file, and surfaces it in the Agent Log panel in the chat toolbar. Default: false. Use during development; omit or set to false for distribution.

Exported functions

An agent exports functions that the player calls at the appropriate times. All functions share the same parameter shape; unused parameters can be omitted from the signature. In Python, the exports are the module-level functions: onLoad is on_load, onToolCall is on_tool_call, and @command handlers use the exact name declared in the manifest. Python functions may be async def or plain def; return-value semantics are identical, with None standing in for null/undefined. Lua follows the same mapping with its global functions (on_load, on_tool_call, verbatim command handlers); handlers are plain functions, and nil stands in for null.

onLoad(store, studio, context)

Called at conversation start if @preload true. Return a string to inject as additional context before the first model turn, or null if there is nothing to inject. Use this to surface current agent state — stored memories, active stats, tracked NPCs — so the model begins the conversation already aware of them.

The player collects all non-null return values from all preloading agents, concatenates them, and sends the result as a user message with isFile: true, after the character context block (see System prompt macros above). It is not appended to the system prompt.

onToolCall(toolName, args, store, studio, context)

Called when the model emits a tool call for a tool this agent owns. toolName is the tool name string. args is a plain object of the parsed arguments. Return a string — this becomes the tool result sent back to the model. The player handles appending the result to the conversation and calling the model again.

Named command handlers

Each @command declaration maps a command token to a named exported function:

// @command @memory memoryAdd
export async function memoryAdd(args, store, studio, context) {
    // args is the string after the command token, unparsed
    // return a string to signal a validation error (dialog stays open)
    // return nothing (undefined) on success
}

Command handlers are invoked directly by the user through the command dialog, without a model call. They may use studio.log() and studio.toast() to give feedback. A returned string is treated as a validation error — the player should display it and keep the dialog open so the user can correct the input.

Command handlers are optional

Note that user-entered commands are useful to manage the model's interactions with agents, but may not be useful for all player situations. You may decide not to implement these handlers in your player, but it is recommended. One common use case for these commands is to update data that the model failed to update (update an event, add a new event, update a time value). In any case, these command handlers being skipped will not change how models interact with agents.

The store object

The store parameter is a plain, JSON-serializable object that belongs entirely to the agent. Its structure is defined by the agent — the player does not interpret it. The player loads the store from persistent storage before each call and saves it back after. The agent reads and mutates it freely; no explicit save call is needed. Python agents receive the store as a real dict and mutate it the same way; the mutated dict is synced back automatically after each call. Lua agents receive it as a real table with the same automatic sync-back — with one caveat from Lua's single table type: a list the agent leaves empty serializes as {} rather than [] (keys that held arrays going in are restored to arrays), so agents in any language reading a shared store should tolerate both empty shapes.

The store starts as {} for a new conversation unless a default state file exists for this agent in the package root, in which case it starts as a copy of that file's contents.

The studio bridge object

The studio parameter is provided by the player. It gives the agent access to platform capabilities. Agents should check for the presence of methods before calling them, since different player implementations may support different subsets of the bridge API. Python agents call the same methods under the same camelCase names (await studio.callModel(...), await studio.context.readFile(...)), and Lua agents do too with :await() in place of the keyword (studio.callModel(...):await()) — this reference applies to all four languages verbatim.

studio.toast(message)

Shows a transient notification to the user. Does not appear in the conversation and is not sent to the model. Players that have no visible UI may ignore this call.

studio.log(message)

Records a persistent audit-trail entry. The entry is shown to the user (typically in the conversation alongside regular messages, but visually distinguished) and is preserved across reloads. It is not included in the messages array sent to the model. Use this for significant agent actions the user should be able to review — "Memory stored: user_name = Marcus", "Event fired: pizza_delivery".

studio.history.search(query)

Searches all conversation turns for turns whose content contains query (case-insensitive). Returns an array of matching turns. Each turn has the shape:

{ role: string, content: string, index: number }

index is the message's zero-based position in the conversation. Each message is indexed individually — index 0 is the first user message, index 1 is the first assistant response, index 2 is the second user message, and so on. The sequence is not guaranteed to alternate perfectly: a user message with no following assistant response is possible if a response failed or was deleted. Only messages with role "user" or "assistant" are returned; internal entries are excluded.

studio.history.range(start, end)

Returns the conversation turns from position start (inclusive) to end (exclusive), using the same zero-based index as search. Returns only "user" and "assistant" turns; internal entries within that range are omitted. context.conversationLength gives the total length; passing it as end retrieves all turns: studio.history.range(0, context.conversationLength).

studio.web.fetch(url, options) — optional

Fetches a web page and returns its readable text content. Requires @api studio.web in the agent's manifest — enforced by the sandbox. This is an optional host capability: not all players implement it. Character Studio stubs this method — every call rejects with a "not implemented" error. Agents that declare @api studio.web still load and run correctly in Character Studio; only the web fetch calls fail. Implementing hosts (such as Character Player) may provide the full implementation behind a per-character configuration gate.

options is optional. Supported option: maxChars — maximum characters of page text to return (default: 8000; implementing hosts clamp to [500, 30000]).

On success, resolves to:

{
    url:         string,       // final URL after redirects
    status:      number,       // HTTP status
    contentType: string,       // e.g. "text/html"
    title:       string|null,  // <title> text for HTML pages, else null
    text:        string,       // readable plain text (scripts/nav/images stripped)
    truncated:   boolean       // true when text was cut at maxChars
}

Agent obligations when using studio.web:

try {
    const page = await studio.web.fetch(url, { maxChars: 8000 });
    return `## ${page.title ?? url}\n\n${page.text}${page.truncated ? "\n\n[content truncated]" : ""}`;
} catch (err) {
    return `Could not read ${url}: ${err.message}`;
    // Do NOT return fabricated page content here
}

The context object

{
    invokedBy:          "model" | "direct",  // tool call or @command
    conversationLength: number,              // total turns in conversation
    characterName:      string,              // character name from config
    agentId:            string,              // this agent's registered ID
    config:             object,              // per-instance config (composed agents only, else {})
}

agentId is the registered ID for this agent instance — useful for agents that log or generate IDs relative to themselves. config is the object from a composed agent entry in config.json (see the composable entry format below); for simple string-registered agents it is always {}. Composed agents use context.config to discover their configuration — for example, context.config.contextFile tells studio.context-access which file to search.

Bridge API versioning

Player implementations vary in which bridge methods they support. Agents that use optional bridge capabilities should check before calling:

// Defensive use of a capability that may not exist in all players
if (studio.conversation?.collapse) {
    await studio.conversation.collapse(upToIndex, summaryText);
} else {
    studio.log("This player does not support conversation collapse.");
}

This is the standard pattern for forward compatibility. Agents that require capabilities not present in the player should return a descriptive error string from onToolCall rather than throwing.

Library files

An agent that needs a third-party JavaScript library declares it with @lib in the manifest. The player loads the library file from agents/lib/{name}.js (falling back to agents/lib/{name}.min.js) and evaluates it in the agent's execution environment before the agent module loads. The library registers on the global scope via the IIFE/UMD pattern, making it available as a global to the agent code.

/**
 * @agent mychar.battle-manager
 * @lib jmespath
 */

// jmespath is now available as a global — no import needed
export function onToolCall(toolName, args, store, studio, context) {
    const results = jmespath.search(store.units, args.filter);
    return JSON.stringify(results);
}

Library format requirements: Library files must be IIFE (Immediately Invoked Function Expression) or UMD (Universal Module Definition) format. These formats self-register on globalThis when evaluated. ES module format (files using import/export as their primary interface) is not supported as library files. Most established libraries provide both an ES module build and a UMD/browser build — use the UMD or browser build.

Environment constraints: Library code runs in the same sandboxed context as the agent — no DOM, no fetch, no localStorage, no window. Pure utility libraries (data manipulation, parsing, math) work without issue. Libraries that require browser or network APIs do not.

Player responsibilities: A conformant player that supports the @lib field must locate agents/lib/{name}.js within the character package and evaluate it in the agent's execution context before evaluating the agent module. Players that run agents in isolated workers must evaluate libraries in the worker's global scope, not the main thread's. Libraries are never sent to the model and never affect the broker loop — they are purely an agent implementation detail.

Writing a library file

A library file wraps its exports in an IIFE and attaches them to globalThis. The pattern below is all that's required. A copy of this file ships as agents/lib/weighted-choice.js in Character Studio's agents/lib/ directory and can be used as a starting template.

/**
 * agents/lib/weighted-choice.js
 *
 * Sample library: weighted random selection from a table.
 * Declare with @lib weighted-choice in your agent manifest.
 *
 * weightedChoice.pick(table)      — pick one entry by weight
 * weightedChoice.pickN(table, n)  — pick n entries with replacement
 *
 * Table format: [{ weight: number, value: any }, ...]
 */

(function (global) {
    "use strict";

    function pick(table) {
        if (!Array.isArray(table) || table.length === 0)
            throw new Error("weightedChoice.pick: table must be a non-empty array");
        const total = table.reduce((sum, e) => sum + (Number(e.weight) || 1), 0);
        let roll = Math.random() * total;
        for (const entry of table) {
            roll -= (Number(entry.weight) || 1);
            if (roll < 0) return entry.value;
        }
        return table[table.length - 1].value;
    }

    function pickN(table, n) {
        const results = [];
        for (let i = 0; i < n; i++) results.push(pick(table));
        return results;
    }

    global.weightedChoice = { pick, pickN };

}(typeof globalThis !== "undefined" ? globalThis : this));

The key points: wrap everything in an IIFE, accept global as a parameter, assign your public API to global.yourLibraryName, and pass typeof globalThis !== "undefined" ? globalThis : this as the argument. That's the entire pattern — no module system, no bundler, no build step required.

Download weighted-choice.js

Agent example skeleton

/**
 * @agent mycharacter.example
 * @version 1.0
 * @description A minimal example agent
 *
 * @tool do_something
 * @tool-description Call this when something happens
 * @tool-params {"type":"object","properties":{"what":{"type":"string"}},"required":["what"]}
 *
 * @command @example exampleCommand
 *
 * @preload true
 */

// store shape: { items: string[] }

export function onLoad(store, studio, context) {
    if (!store.items?.length) return null;
    return `## Example Items\n${store.items.map(i => `- ${i}`).join("\n")}`;
}

export function onToolCall(toolName, args, store, studio, context) {
    if (toolName === "do_something") {
        if (!store.items) store.items = [];
        store.items.push(args.what);
        studio.log(`Example: stored "${args.what}"`);
        return `Stored: ${args.what}`;
    }
    return `[Unknown tool: ${toolName}]`;
}

export async function exampleCommand(args, store, studio, context) {
    if (!args.trim()) return "Usage: example <something>";
    if (!store.items) store.items = [];
    store.items.push(args.trim());
    studio.toast(`Stored: ${args.trim()}`);
}

Download sample-agent.js

---

Part 2 — Player Implementors

A player is any software that loads a character package, manages a conversation, and runs agents. Character Studio is one player implementation. This section describes what a conformant player must do.

Loading a character package

On startup, the player:

  1. Reads config.json and builds a path resolver from fileMappings (see the Character Package section above). All subsequent file reads use this resolver — never hard-coded default paths.
  2. Reads the character content files through the resolver: instructions, system prompt, description, intro, and all files in the context directory.
  3. Parses the "agents" array and for each agent ID, loads the corresponding .js file from the package's agents/ directory.
  4. Parses each agent's manifest comment block to extract tools, commands, and flags.
  5. Registers the agent in load order. On any tool name or command name collision, the first registered agent wins and a warning should be logged. Later registrations for the same name are silently skipped.
  6. For agents with @preload true, loads their persistent store (from the conversation's saved state, or from the default state file if this is a new conversation) and calls onLoad. Concatenates all non-null return values and sends the result as a user message with isFile: true, after the character context block.

The tool definitions array

Before each API call, the player builds a tools array from all registered agent manifests and includes it in the request body alongside the messages array. Each tool entry follows the OpenAI function-calling format:

{
    "type": "function",
    "function": {
        "name": "tool_name",
        "description": "From @tool-description",
        "parameters": { /* From @tool-params — a JSON Schema object */ }
    }
}

The broker loop

After sending the messages and tools to the LLM, the player enters the broker loop:

  1. Stream the response. If finish_reason is "stop", the loop ends — deliver the final response to the consumer.
  2. If finish_reason is "tool_calls":
    1. Append the assistant's tool-call message to the conversation: { role: "assistant", tool_calls: [...] }
    2. For each tool call in the response, find the registered agent that owns that tool name. If no agent owns it, return a tool result message with an error string.
    3. Load that agent's store if not already loaded. Call agent.onToolCall(toolName, args, store, studio, context). The returned string is the tool result. Save the (potentially mutated) store back to persistent storage.
    4. Append a tool result message to the conversation: { role: "tool", tool_call_id: "...", content: "result string" }
  3. After all tool calls in this round are dispatched, go back to step 1 — rebuild the messages array and call the LLM again.

The loop terminates when the model returns a "stop" finish reason. Players should enforce a maximum iteration count (recommended: 10) to prevent runaway loops in the event of a misbehaving model or agent.

Persistent storage

The player is responsible for persisting:

Agent stores are conversation-scoped. Switching to a different conversation gives each agent a fresh store (seeded from the default state file if one exists). An agent's store from one conversation is never visible to another.

The studio bridge object

The player constructs a studio object and passes it to every agent call. At minimum, a conformant player must implement:

MethodRequiredBehavior
studio.toast(message)YesShow a transient notification. Headless players may implement this as a no-op.
studio.log(message)YesRecord a persistent audit entry in the conversation. Must survive reloads. Must not be sent to the model.
studio.history.search(query)YesCase-insensitive substring search over user/assistant turns. Returns [{ role, content, index }] where index is zero-based.
studio.history.range(start, end)YesReturns user/assistant turns in the given zero-based index range (start inclusive, end exclusive). index in each result is the position in the full conversation.
studio.web.fetch(url, options)No — optionalFetches a web page and returns readable text. Players that do not provide web access must reject with an error message containing the phrase "not implemented". Character Studio uses this stub response. See rejection conditions below.

studio.web rejection conditions

Players that implement studio.web.fetch must reject (throw) for the conditions below. Players that do not implement it must always reject with the canonical "not implemented" response. Agents must catch all of these — see Part 1.

ConditionError message must contain
Host does not provide web access"not implemented" — the canonical stub response
Agent did not declare @api studio.webEnforced by the sandbox before the host is reached
Invalid URL or non-http(s) scheme"not a valid URL" or "only http(s)"
Private-network target (SSRF protection)"blocked"
Timeout, network failure, or unsupported content typeHost-specific detail

Additional bridge methods may be added in future API versions. Players should implement new methods as they are defined, but agents must not require them without defensive capability checks (see Part 1).

The context object

The player constructs a fresh context object for each agent call:

{
    invokedBy:          "model" | "direct",
    conversationLength: number,
    characterName:      string,
    agentId:            string,   // registered ID of the agent being called
    config:             object,   // per-instance config from composed entry, or {}
}

invokedBy is "model" for broker-loop tool calls and "direct" for command dialog invocations. conversationLength is the number of entries in the conversation at the time of the call. characterName is from config.json. agentId is the registered ID for this instance. config is the full object from a composed agent entry in config.json; for simple string-registered agents it is {}.

Agent sandboxing

Agent code is arbitrary JavaScript provided by character authors. Players that run agents in the main application thread accept full trust of that code. Players that serve public-facing deployments with characters from unknown authors should run each agent in an isolated execution context (such as a Web Worker or equivalent) with the following constraints:

The @api manifest declarations are intended to become permission gates in this model — an agent that declares @api studio.history receives history methods over the channel; one that does not cannot call them even if it tries. This enforcement is not required in trusted (single-author) deployments.

Python agents ride the same sandbox: Studio and the web player run them in a CPython-on-WebAssembly interpreter (Pyodide) inside the same worker, behind the same message protocol and @api gates. A player adding Python support hosts the runtime and routes language: "python" loads to it; the agent-facing contract is unchanged. Packages ship .py source as-is — there is no compiled form.

Lua agents follow the identical pattern with a far smaller runtime: real Lua 5.4 on WebAssembly (Wasmoon, ~420 KB), loaded lazily behind language: "lua" loads, with the agent environment restricted to the portable subset (no io, no require/load, no debug — the bridge is the only door). Packages ship .lua source as-is.

---

Part 3 — Character Consumers

A consumer is any website or application that embeds a character using a player library. This section describes the interface a player library should expose to consumers, and what consumers need to do to integrate it.

Note

No standalone player library currently exists — Character Studio implements the player internally. This section describes the intended future interface so that consumer integrations can be written against a stable contract as the library is developed.

Initialization

A consumer provides a character package and API credentials. The player handles everything else.

const player = new CharacterPlayer({
    package:  "/path/to/character-package",  // or a pre-loaded package object
    apiKey:   "your-api-key",
    baseUrl:  "https://api.venice.ai/api/v1", // optional override
    model:    "model-id",                     // optional override
});

Sending a message

The consumer sends a user message and receives the completed assistant response. All tool calls, broker loop iterations, and agent activity happen internally — the consumer sees only the final response.

const response = await player.send("Hello, how are you?");
// response: { role: "assistant", content: "I'm doing well..." }

Events

Consumers can subscribe to player events for richer integration:

EventPayloadWhen
message{ role, content }Final assistant response delivered
tokenstringStreaming token (if streaming is enabled)
agentLogstringAn agent called studio.log()
agentToaststringAn agent called studio.toast()
toolCall{ agentId, toolName, result }An agent tool call completed
errorErrorAPI error or agent error

Conversation management

// Start a new conversation (existing conversation is saved automatically)
await player.newConversation();

// Get the current conversation as an array of turns
const turns = player.getConversation();

// Load a previously saved conversation by ID
await player.loadConversation(id);

// List all saved conversations for this character
const list = await player.listConversations();

What consumers do not need to know

The agent system is entirely internal to the player. Consumers do not need to know which agents a character uses, how to configure them, or that tool-calling is happening at all. A consumer that only uses player.send() and listens for message events gets a fully functional character without any awareness of the underlying machinery.