> ## Documentation Index
> Fetch the complete documentation index at: https://docs.oumi.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# RUN YOUR AGENT AS A SUBAGENT IN PI

> Keep your usual model in pi and give it your fine-tuned Oumi agent as a tool

Your agent can work for the model that you already use in pi. You keep your usual model, for example a frontier model, as the main model of the pi session. A pi extension adds one tool that sends a question to your fine-tuned agent on Oumi. Your agent runs its own tool loop with the system prompt and tools from its training data, and returns its answer to the main model as the tool result.

You do not switch models or sessions when work needs your agent. The main model makes one tool call, and your small fine-tuned model does the multi-step tool calling at a fraction of the cost and latency of a frontier model.

This guide uses the same stock lookup agent as [Run your agent as the main model](https://docs.oumi.ai/guides/agents/run-your-agent/pi/main-model.md). The agent has two tools: `search_company` finds the ticker symbol for a company name, and `get_stock_price` returns the latest price for a ticker. The main model gets one tool, `ask_stock_agent`.

***

## HOW THE PIECES CONNECT

![The pi agent loop sends your prompt to your main model, which calls ask\_stock\_agent. The subagent extension runs its own loop against your Oumi deployment, runs each stock tool against your data sources, and returns the answer to the pi agent loop](https://mintcdn.com/oumi/up4xn2fDkJQDzAD6/images/pi-subagent-light.svg?fit=max\&auto=format\&n=up4xn2fDkJQDzAD6\&q=85\&s=4b2b810901e1845cdaf037760c651fa4)

The two models do not see each other's context:

* **The main model** sees pi's system prompt, its own tools, and `ask_stock_agent`. It does not see the stock tools or your agent's system prompt.
* **Your fine-tuned model** sees only the requests that the subagent sends: the system prompt from your training data, the stock tools, and the subagent's own history. It does not see pi's prompt, pi's tools, or your conversation with the main model.

Your fine-tuned model therefore gets the same context as in training, even though pi gives the main model coding tools and a coding system prompt.

***

## BEFORE YOU START

You will need:

* An Oumi project.
* pi, installed on your machine by following the [pi quickstart](https://pi.dev) instructions, with a main model that you already use.
* A personal Oumi API key from **User Settings > API Keys**.
* The system prompt from your agent's training data.

***

**1. Train and deploy a tool-calling model**

Follow the first two steps of [Run your agent as the main model](https://docs.oumi.ai/guides/agents/run-your-agent/pi/main-model.md): train a tool-calling model, deploy it, and save your Oumi API key in `~/.config/oumi/api.key`. Copy the model path of the deployment, for example `projects/{project_id}/deployments/{deployment_id}`.

**2. Create the extension directory**

pi loads a subdirectory of `~/.pi/agent/extensions/` that contains an `index.ts` file as one extension. Create the directory, and save the system prompt from your training data in it:

```bash theme={null}
mkdir -p ~/.pi/agent/extensions/stock-agent
cp ./stock-agent-system-prompt.md ~/.pi/agent/extensions/stock-agent/system-prompt.md
```

When you finish, the directory contains four files:

| File | Contents |
| - | - |
| `system-prompt.md` | The system prompt from your training data |
| `tools.ts` | The definitions of the stock tools, and the code that runs them |
| `agent.ts` | The subagent's tool loop against your Oumi deployment |
| `index.ts` | The pi extension that gives the main model the `ask_stock_agent` tool |

> [!WARNING]
>
> If you followed the main model guide, remove `~/.pi/agent/extensions/stock-tools.ts`. Otherwise pi also gives the stock tools to the main model, and the main model can call them directly instead of asking your agent.

**3. Define and implement the tools**

pi does not run your agent's tools in this setup. The subagent sends the tool definitions with each request to your deployment, and runs each tool call itself.

Write each definition in the OpenAI tools format from its Oumi tool. Copy the Oumi `toolName` into `name`, and copy `description` and `parameters` without changes. The model learned your tools from these definitions.

Create `~/.pi/agent/extensions/stock-agent/tools.ts`:

```typescript tools.ts theme={null}
const API = "https://api.example-stocks.com";

// Copies of the Oumi tool definitions. A name, parameter, or description that
// differs from training makes the model call tools that do not exist.
export const TOOLS = [
	{
		type: "function",
		function: {
			name: "search_company",
			description: "Find a company's stock ticker symbol by company name. Call this before get_stock_price.",
			parameters: {
				type: "object",
				properties: { name: { type: "string", description: "Company name, e.g. 'Apple'." } },
				required: ["name"],
			},
		},
	},
	{
		type: "function",
		function: {
			name: "get_stock_price",
			description: "Get the latest stock price for a ticker symbol returned by search_company.",
			parameters: {
				type: "object",
				properties: { ticker: { type: "string", description: "Stock ticker symbol, e.g. 'AAPL'." } },
				required: ["ticker"],
			},
		},
	},
];

export async function runTool(name: string, args: Record<string, string>, signal?: AbortSignal): Promise<string> {
	if (name === "search_company") return searchCompany(args.name, signal);
	if (name === "get_stock_price") return getStockPrice(args.ticker, signal);
	throw new Error(`Unknown tool: ${name}`);
}

async function searchCompany(name: string, signal?: AbortSignal): Promise<string> {
	const res = await fetch(`${API}/search?q=${encodeURIComponent(name)}`, { signal });
	if (res.status === 404) return `No company found matching "${name}".`;
	if (!res.ok) throw new Error(`Search returned HTTP ${res.status}`);

	const { ticker, name: found } = (await res.json()) as { ticker: string; name: string };
	return `Found ${found} (ticker: ${ticker}).`;
}

async function getStockPrice(ticker: string, signal?: AbortSignal): Promise<string> {
	const res = await fetch(`${API}/quote/${encodeURIComponent(ticker)}`, { signal });
	if (res.status === 404) return `No price found for "${ticker}".`;
	if (!res.ok) throw new Error(`Quote returned HTTP ${res.status}`);

	const { price, currency } = (await res.json()) as { price: number; currency: string };
	return `${ticker}: ${price} ${currency}.`;
}
```

A tool that finds no match returns a result that says so. A tool whose data source fails throws an error, and the subagent sends that error to the model as the tool result. Neither path invents a value.

**4. Write the subagent's tool loop**

The subagent does for your fine-tuned model what pi does for a main model. It sends the conversation and the tool definitions to your deployment, runs each tool call in the reply, adds the results to the conversation, and repeats until the model answers without a tool call.

Create `~/.pi/agent/extensions/stock-agent/agent.ts`, and set `MODEL` to the model path of your deployment:

```typescript agent.ts theme={null}
import { readFileSync } from "node:fs";

import { runTool, TOOLS } from "./tools";

const ENDPOINT = "https://inference.oumi.ai/v1/chat/completions";
const MODEL = "projects/{project_id}/deployments/{deployment_id}";

// Stops a model that keeps calling tools. Set it above the number of tool
// calls that your longest training conversation makes.
const MAX_TOOL_STEPS = 8;

const SYSTEM_PROMPT = readFileSync(new URL("./system-prompt.md", import.meta.url), "utf8");

type Message = Record<string, unknown>;
type ToolCall = { id: string; type: "function"; function: { name: string; arguments: string } };

export class StockAgent {
	private history: Message[] = [{ role: "system", content: SYSTEM_PROMPT }];

	constructor(private readonly apiKey: string) {}

	// A turn that fails part way leaves tool calls without results, and your
	// deployment rejects a conversation like that. Removing the failed turn
	// keeps the next question working.
	async ask(question: string, onToolCall: (call: string) => void, signal?: AbortSignal): Promise<string> {
		const start = this.history.length;
		try {
			return await this.turn(question, onToolCall, signal);
		} catch (error) {
			this.history.length = start;
			throw error;
		}
	}

	private async turn(question: string, onToolCall: (call: string) => void, signal?: AbortSignal): Promise<string> {
		this.history.push({ role: "user", content: question });

		for (let step = 0; step < MAX_TOOL_STEPS; step++) {
			const reply = await this.complete(signal);
			const calls: ToolCall[] = reply.tool_calls ?? [];

			if (calls.length === 0) {
				this.history.push({ role: "assistant", content: reply.content });
				return reply.content;
			}

			this.history.push({ role: "assistant", content: reply.content ?? "", tool_calls: calls });
			for (const call of calls) {
				onToolCall(`${call.function.name}(${call.function.arguments})`);
				this.history.push({ role: "tool", tool_call_id: call.id, content: await runToolCall(call, signal) });
			}
		}

		throw new Error(`The stock agent made ${MAX_TOOL_STEPS} rounds of tool calls without an answer.`);
	}

	private async complete(signal?: AbortSignal): Promise<{ content: string; tool_calls?: ToolCall[] }> {
		const res = await fetch(ENDPOINT, {
			method: "POST",
			headers: { "Content-Type": "application/json", Authorization: `Bearer ${this.apiKey}` },
			body: JSON.stringify({ model: MODEL, messages: this.history, tools: TOOLS }),
			signal,
		});
		if (!res.ok) throw new Error(`Oumi returned HTTP ${res.status}: ${await res.text()}`);

		const body = (await res.json()) as { choices: { message: { content: string; tool_calls?: ToolCall[] } }[] };
		return body.choices[0].message;
	}
}

// A failed tool call goes back to the model as its result, as it does in pi,
// so the model can tell the user instead of inventing a value.
async function runToolCall(call: ToolCall, signal?: AbortSignal): Promise<string> {
	try {
		return await runTool(call.function.name, JSON.parse(call.function.arguments), signal);
	} catch (error) {
		return `Error: ${error instanceof Error ? error.message : String(error)}`;
	}
}
```

**5. Give the main model the subagent as a tool**

The main model decides when to call `ask_stock_agent` from the tool's description alone. Say what your agent can answer and what it remembers, so that the main model calls it for the right questions.

Create `~/.pi/agent/extensions/stock-agent/index.ts`:

```typescript index.ts theme={null}
import { readFileSync } from "node:fs";
import { homedir } from "node:os";
import { join } from "node:path";

import { Type } from "@earendil-works/pi-ai";
import { defineTool, type ExtensionAPI } from "@earendil-works/pi-coding-agent";

import { StockAgent } from "./agent";

const API_KEY_FILE = join(homedir(), ".config/oumi/api.key");

export default function (pi: ExtensionAPI) {
	// One subagent per pi session. It keeps its own history, so a follow-up
	// question can refer to a company that it already found.
	let agent: StockAgent | undefined;
	pi.on("session_start", () => {
		agent = undefined;
	});

	const askStockAgent = defineTool({
		name: "ask_stock_agent",
		label: "Stock Agent",
		description:
			"Ask the stock agent for the latest stock price of one or more companies. It finds the " +
			"ticker symbols itself and answers in prose. It remembers earlier questions in this " +
			"session, so a follow-up can refer to a company it already found. Set reset to true to " +
			"start a new conversation with it.",
		parameters: Type.Object({
			question: Type.String({ description: "The question, in plain English." }),
			reset: Type.Optional(Type.Boolean({ description: "Forget earlier questions before this one." })),
		}),

		async execute(_toolCallId, params, signal, onUpdate) {
			if (!agent || params.reset) agent = new StockAgent(readFileSync(API_KEY_FILE, "utf8").trim());

			// Shows each of the subagent's tool calls in pi while it works.
			const calls: string[] = [];
			const answer = await agent.ask(
				params.question,
				(call) => {
					calls.push(call);
					onUpdate?.({ content: [{ type: "text", text: calls.join("\n") }], details: { calls } });
				},
				signal,
			);

			return { content: [{ type: "text", text: answer }], details: { calls } };
		},
	});

	pi.registerTool(askStockAgent);
}
```

The subagent reads your API key when the main model first calls `ask_stock_agent`, so a pi session that never asks your agent does not need the key.

**6. Ask a question in pi**

Start pi as you usually do, with your usual model. If pi is already running, run `/reload`. Then ask a question such as "Is Nucor trading above 150 today?".

The main model calls `ask_stock_agent`. While your agent works, pi shows each of its tool calls under the `ask_stock_agent` call: `search_company` to find the ticker `NUE`, then `get_stock_price` with that ticker. The main model then answers you from your agent's reply.

Ask a follow-up such as "And how about Apple?". The main model calls `ask_stock_agent` again, and your agent answers with the conversation from the first question still in its history.

***

## TROUBLESHOOTING

* **`ask_stock_agent` fails with `429` and "Deployment is warming up":** Your deployment uses [scale-to-zero](https://docs.oumi.ai/guides/deployment/deploy-a-model.md#autoscaling-and-cost-custom-models) and has no warm replica. The subagent does not wait for a cold start. Wait a few minutes and ask again, or set min replicas to 1.
* **`ask_stock_agent` fails with `400` and "Invalid model format":** `MODEL` in `agent.ts` is not a model path. Set it to `projects/{project_id}/deployments/{deployment_id}`.
* **`ask_stock_agent` fails with `ENOENT` for `api.key`:** The subagent cannot read your Oumi API key. Save the key in `~/.config/oumi/api.key`.
* **`ask_stock_agent` does not appear in pi:** pi did not load the extension. Make sure the file is named `index.ts` and is in `~/.pi/agent/extensions/stock-agent/`, then run `/reload`.
* **The main model answers stock questions without calling `ask_stock_agent`:** The main model does not know when to call your agent. Make the tool's description more specific about the questions that your agent answers, then run `/reload`.
* **The main model calls `search_company` or `get_stock_price` itself:** pi also loads the stock tools from another extension. Remove `~/.pi/agent/extensions/stock-tools.ts`.
* **Your agent answers from an earlier, unrelated conversation:** The subagent keeps its history for the whole pi session. Ask the main model to reset the stock agent, so that it calls `ask_stock_agent` with `reset` set to `true`.

***

## WHAT'S NEXT

> **[Run your agent as the main model](https://docs.oumi.ai/guides/agents/run-your-agent/pi/main-model.md)**
>
> Select your fine-tuned model in pi and give it only your tools.

> **[Inference logs](https://docs.oumi.ai/guides/deployment/inference-logs.md)**
>
> Review the requests that the subagent sent to your deployment.

> **[Compounding intelligence](https://docs.oumi.ai/guides/deployment/compounding-intelligence.md)**
>
> Find where your deployed model struggles, and retrain it on targeted data.

> **[Agent harnesses](https://docs.oumi.ai/reference/agent-harnesses.md)**
>
> Learn what an agent harness contains, including subagents.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.