Placement
Where an Agent runs: backend, runOn, and toolsRunOn.
Placement answers one question — where does this run? — along two axes: the whole agent (runOn / backend) or only its tools (toolsRunOn).
Quick Start
- A custom backend runs the whole agent
typescript import { Agent } from 'praisonai'; const agent = new Agent({ instructions: 'Hosted agent', backend: { async execute(prompt) { return `handled remotely: ${prompt}`; }, }, }); await agent.chat('Do the work'); - Only the tools run elsewhere
typescript import { Agent } from 'praisonai'; // Both 'local' and 'docker' are registered automatically on import. const agent = new Agent({ instructions: 'Local loop', toolsRunOn: 'local' }); - Run a tool inside Docker
typescript import { Agent, ComputeToolPlace, DockerCompute } from 'praisonai'; // A place carries a command template per tool. 'greet' knows how to run itself. const place = new ComputeToolPlace(new DockerCompute(), { greet: 'echo hello-{{who}}', }); const agent = new Agent({ instructions: 'Greeter', toolsRunOn: place }); // When the model calls greet({ who: 'world' }), the shell command runs inside // Docker and its stdout comes back as the tool result. setCommand(name, template) // declares a tool's command after construction.
The two axes
| Option | Type | Meaning | |
|---|---|---|---|
runOn | `string \ | ManagedBackendLike` | Names a managed runtime that hosts the whole agent — model calls, loop and tools. runOn: 'x' is shorthand for backend: <hosted agent for x>. |
backend | ManagedBackendLike | A custom object exposing execute(prompt, options) (and optional stream(), managedSessionId). | |
toolsRunOn | `string \ | ToolPlaceLike` | Names a tool place — the tools run there, thinking stays local. |
ManagedBackendLike and ToolPlaceLike are exported interfaces. The default place, LOCAL_TOOL_PLACE, runs a tool unchanged in this process.
Exclusion rules
The two axes are disjoint, so naming two places for the same thing is a typo worth catching. Any of these throws at construction:
Before this change a mis-typed name survived construction and only surfaced on the first model turn — after the prompt was built and, for a real run, after the API spend. Validation now happens at the call site.
Registering hosts
A hosted runtime and a remote tool place are provider integrations that do not ship in the package. A host fills the registries:
import { registerManagedRuntime, registerToolPlace } from 'praisonai';
registerManagedRuntime('my-runtime', () => ({
async execute(prompt) { return `ran ${String(prompt)}`; },
}));
registerToolPlace('my-sandbox', () => ({
placeName: 'my-sandbox',
async runTool(_name, _args, run) { return run(); },
}));Once registered, runOn: 'my-runtime' and toolsRunOn: 'my-sandbox' resolve by name. unregisterManagedRuntime / unregisterToolPlace undo a registration (local cannot be removed).
registerComputeProvider('e2b', …) now also registers a matching placement entry, so new Agent({ toolsRunOn: 'e2b' }) works after a single call — a caller no longer has to remember two registrations. The two move together.
Where a tool call goes
A tool runs on the provider only when it declares a command template; a tool without one runs locally and says so, rather than implying isolation it does not have.
graph TB
T[🔧 Tool call] --> H{Has command<br/>template?}
H -->|No| L[💻 Local fallback<br/>says so]
H -->|Yes| A{Provider<br/>available?}
A -->|No| R1[❌ Raise ComputeError]
A -->|Yes| E[▶️ Run in provider]
E --> O{Result}
O -->|exit 0| S[✅ stdout returned]
O -->|non-zero exit| R2[❌ Raise ComputeError]
O -->|timeout| R3[❌ Raise ComputeError]
classDef call fill:#8B0000,stroke:#7C90A0,color:#fff
classDef check fill:#F59E0B,stroke:#7C90A0,color:#fff
classDef local fill:#6366F1,stroke:#7C90A0,color:#fff
classDef run fill:#189AB4,stroke:#7C90A0,color:#fff
classDef ok fill:#10B981,stroke:#7C90A0,color:#fff
classDef err fill:#8B0000,stroke:#7C90A0,color:#fff
class T call
class H,A,O check
class L local
class E run
class S ok
class R1,R2,R3 errThe shorthand toolsRunOn: 'docker' builds a ComputeToolPlace for you, but with no command templates — so every tool falls through to its local implementation. That fallback warns via console.warn, once per tool name on each place (a hot loop can’t spam it), so a caller who asked for a real sandbox ('docker', 'e2b', …) gets a visible signal instead of a silent downgrade. A 'local' place skips the warning — running on the host is what 'local' means, so a warning there would be noise:
[praisonai] tool '<toolName>' has no command mapping for place '<placeName>',
so it runs on the HOST, not in the sandbox.
Give it a shell command (place.setCommand('<toolName>', '...')) to run it there.To route a specific tool remotely, build a ComputeToolPlace with the command template as in the Quick Start (or call setCommand(name, template)).
Compute providers behind toolsRunOn
toolsRunOn is powered by the compute-provider registry. praisonai-ts ships two providers — 'local' and 'docker' — via LocalCompute and DockerCompute.
import { listComputeProviders, registerComputeProvider } from 'praisonai';
listComputeProviders(); // ['docker', 'local']
registerComputeProvider('my-remote', () => new MyRemoteCompute());listComputeProviders() returns what’s available in this build; registerComputeProvider(name, factory) adds a remote one without touching the SDK. The 'local' and 'docker' tool places resolve automatically on first use via ensureBuiltinToolPlaces() — no import step is needed at all for those names to work. new Agent({ toolsRunOn: 'local' }) and new Agent({ toolsRunOn: 'docker' }) both work out of the box; the placement registry’s toolPlaceNames() returns ['docker', 'local'] in a stock build. An unknown name (for example 'e2b') still raises rather than silently running on the host. See Compute Providers (TypeScript).
Reading the resolved placement
import { Agent } from 'praisonai';
const agent = new Agent({ instructions: 'x', toolsRunOn: 'local' });
console.log(agent.getManagedBackend()); // undefined
console.log(agent.getToolPlace()); // the 'local' placeTeam-wide toolsRunOn
An AgentTeam can share one sandbox across every member — one ToolPlace instance for the whole team, not one per member.
import { Agent, AgentTeam } from 'praisonai';
const a = new Agent({ instructions: 'Collect data' });
const b = new Agent({ instructions: 'Summarise it' });
const team = new AgentTeam({ agents: [a, b], toolsRunOn: 'local' });
a.getToolPlace() === b.getToolPlace(); // true — same instanceEvery member that declared no toolsRunOn of its own adopts the team’s place, so state a tool leaves persists between members’ tool calls in the same sandbox. A member’s own toolsRunOn wins — it keeps its place. An unknown place name throws at team construction.
// Precedence — a member's own place wins
const own = new Agent({ instructions: 'Sandboxed', toolsRunOn: 'local' });
new AgentTeam({ agents: [own], toolsRunOn: 'docker' }); // own keeps 'local'
// Unknown place → throws at construction
new AgentTeam({ agents: [a, b], toolsRunOn: 'nowhere' }); // TypeErrorThe team’s place also reaches an agent introduced on a task (TeamTask.agent) or built lazily from agentConfig — an agent that never appeared in the agents array at construction. It joins the same shared sandbox on first use.
import { Agent, AgentTeam } from 'praisonai';
const member = new Agent({ instructions: 'Collect data' });
const team = new AgentTeam({ agents: [member], toolsRunOn: 'local' });
// A second agent that lives on a task — never in the team's agents array
const taskOnly = new Agent({ instructions: 'Also runs' });
team.addTask({ agent: taskOnly, prompt: 'do it' });
// On first use, taskOnly joins the SAME sandbox as member
taskOnly.getToolPlace() === member.getToolPlace(); // trueUnder the hood, the team calls Agent.adoptToolPlace(place) on each member. It returns false when the agent already has its own place (its own toolsRunOn or one adopted earlier — a safe no-op), and true when adopted. The call is idempotent, so a member that already has a place is never overridden. See AgentTeam for the full team-level option list.