TypeScript files in the agent's `src/` directory are compiled and registered automatically. Every exported function becomes a **Yao Process** — callable by name from hooks, MCP tools, other scripts, or directly via `yao run`.
## Naming Convention
The process name is derived from the file path:
```
assistants/<namespace>/<agent-id>/src/<filename>.ts
↓ ↓ ↓
agents.<namespace>.<agent-id>.<filename>.<FunctionName>
```
**Examples** (for an agent at `assistants/myns/myagent/`):
| File | Exported function | Process name |
|------|------------------|--------------|
| `src/logic.ts` | `Save` | `agents.myns.myagent.logic.Save` |
| `src/logic.ts` | `Search` | `agents.myns.myagent.logic.Search` |
> **Note:** `src/index.ts` is special — its `Create` and `Next` exports are loaded as **Hook functions**, not registered as Processes. Only non-index scripts become Processes.
Sub-directory files follow the same rule:
| File | Process prefix |
|------|---------------|
| `src/logic/core.ts` | `agents.myns.myagent.logic.core.<Fn>` |
| `src/utils/format.ts` | `agents.myns.myagent.utils.format.<Fn>` |
> **Note:** Internal helper files whose functions are not intended to be called externally should avoid exporting those functions — only the top-level file should export the public API.
## Runtime APIs
Import from `@yao/runtime`:
```typescript
import {
Process, // call any Yao process by name
Exception, // throw structured errors
Authorized, // get the current user/team context
log, // structured logging
Store, // key-value store (shared across requests)
FS, // file system access
Query, // raw SQL queries
} from "@yao/runtime";
```
### `Process(name, ...args)`
Call any Yao process — models, other scripts, built-ins:
```typescript
// Query your agent's model
const MODEL = "agents.myns.myagent";
const records = Process(`models.${MODEL}.record.Get`, {
wheres: [{ column: "team_id", value: teamId }],
limit: 20
});
// Call another script in the same agent
const result = Process("agents.myns.myagent.logic.Save", teamId, data);
// Call a built-in process
const html = Process("utils.http.Get", "https://example.com");
```
### `Authorized()`
Returns the current user and team context. Call this in MCP adapter functions to inject `team_id` without exposing it to the LLM:
```typescript
import { Authorized, Exception } from "@yao/runtime";
function getTeamId(): string {
const auth = Authorized();
if (auth?.team_id) return auth.team_id;
throw new Exception("team_id required", 400);
}
```
### `Exception(message, code)`
Throw a structured error. The code is the HTTP status code returned to the caller:
```typescript
throw new Exception("title is required", 400);
throw new Exception("entry not found", 404);
throw new Exception("internal error", 500);
```
### `log`
```typescript
log.Trace("processing record", { id });
log.Info("record saved", { id, title });
log.Warn("field missing, skipping", { field });
log.Error("unexpected error", { error: e.message });
```
## Two-Layer Pattern
The most common pattern is a two-layer structure:
**Layer 1 — Internal helpers** (`src/logic/core.ts`, not exported as Processes):
```typescript
export function saveRecord(teamId: string, data: RecordData): Result {
const MODEL = "agents.myns.myagent";
return Process(`models.${MODEL}.record.Save`, { team_id: teamId, ...data });
}
```
**Layer 2 — Public API + MCP adapters** (`src/logic.ts`, exported as Processes):
```typescript
// registered as agents.myns.myagent.logic.*
import { saveRecord } from "./logic/core";
import { Authorized, Exception } from "@yao/runtime";
// Process — callable with explicit teamId (e.g. from a hook)
export function Save(teamId: string, data: RecordData): Result {
if (!data?.title) throw new Exception("title is required", 400);
return saveRecord(teamId, data);
}
// MCP adapter — called by the LLM (no teamId in arguments)
export function MCPSave(params: { title: string; content: string }): Result {
if (!params.title) throw new Exception("title is required", 400);
const auth = Authorized();
if (!auth?.team_id) throw new Exception("unauthorized", 401);
return Save(auth.team_id, params);
}
```
`team_id` is resolved server-side via `Authorized()` — the LLM never sees or provides it.
## Calling Scripts from a Hook
```typescript
// src/index.ts
import { Process } from "@yao/runtime";
export function Next(ctx, payload) {
const content = payload.completion?.content as string;
const result = Process(
"agents.myns.myagent.logic.Save",
ctx.authorized.team_id,
{ title: "...", content }
);
ctx.Send({ type: "text", props: { content: `Saved: ${result.id}` } });
return null;
}
```
## Unit Testing
Scripts support built-in unit tests. Name the test file `<name>_test.ts` alongside the source file, export functions named `Test<Something>`, and run with `yao agent test`.
### Test File
```typescript
// src/logic_test.ts
// @ts-nocheck
import { testing, agent } from "@yao/runtime";
import { Save, Search } from "./logic";
export function TestSave(t: testing.T, ctx: agent.Context) {
const teamId = ctx.authorized?.team_id || "test-team";
const result = Save(teamId, { title: "Hello", content: "World" });
t.assert.True(result.id > 0, "id should be assigned");
t.assert.Equal(result.title, "Hello", "title should match");
}
export function TestSave_MissingTitle(t: testing.T, ctx: agent.Context) {
let threw = false;
try {
Save("test-team", { title: "" } as any);
} catch (e: any) {
threw = true;
t.assert.Contains(e.message, "title is required");
}
t.assert.True(threw, "should throw when title is empty");
}
export function TestSearch(t: testing.T, ctx: agent.Context) {
const teamId = ctx.authorized?.team_id || "test-team";
const results = Search(teamId, { limit: 5 });
t.assert.True(Array.isArray(results), "should return an array");
}
```
### Context File
Provide a test context JSON so `ctx.authorized` is populated:
```json
// tests/ctx.json
{
"chat_id": "test-chat",
"authorized": {
"user_id": "test-user",
"team_id": "test-team"
}
}
```
### Run Tests
```bash
# Run all tests in a script file
yao agent test -i scripts.myns.myagent.logic \
--ctx ./assistants/myns/myagent/tests/ctx.json -v
# Run a single test function (--run accepts a regex pattern)
yao agent test -i scripts.myns.myagent.logic \
--ctx ./assistants/myns/myagent/tests/ctx.json -v --run TestSave
```
The `-i` flag takes `scripts.<assistant-path>.<module>` format (not the process name or file path). `-v` enables verbose output. `--run` accepts a regex pattern to filter test functions.
### Assert Methods
| Method | Description |
|--------|-------------|
| `t.assert.True(v, msg?)` | Assert value is truthy |
| `t.assert.False(v, msg?)` | Assert value is falsy |
| `t.assert.Equal(actual, expected, msg?)` | Deep equality |
| `t.assert.NotEqual(actual, expected, msg?)` | Not equal |
| `t.assert.Nil(v, msg?)` | Assert null or undefined |
| `t.assert.NotNil(v, msg?)` | Assert not null/undefined |
| `t.assert.Contains(str, substr, msg?)` | String contains |
| `t.assert.Len(v, n, msg?)` | Array/string length |
| `t.assert.Greater(a, b, msg?)` | `a > b` |
| `t.assert.Match(v, pattern, msg?)` | Regex match |
| `t.log(msg)` | Print a log line |
| `t.skip(reason?)` | Skip this test |
| `t.fail(reason?)` | Mark test as failed (test continues running) |
| `t.fatal(reason?)` | Mark test as failed and stop immediately |
## What's Next
With scripts in place, expose them to the LLM as MCP tools:
- **[MCP Tools (Yao Process) →](./mcp)**