Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/cuddly-days-draw.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@mastra/server': minor
---

Update peer dependencies to match core package version bump (1.11.0)
14 changes: 14 additions & 0 deletions .changeset/five-pots-arrive.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
'@mastra/server': minor
'@mastra/core': patch
---

Added server routes for Agent orchestration features:

- `GET /api/agents/:agentId/modes` — list configured modes and current mode
- `POST /api/agents/:agentId/modes/switch` — switch to a different mode
- `GET /api/agents/:agentId/state` — get agent state
- `POST /api/agents/:agentId/state` — update agent state
- `POST /api/agents/:agentId/send` — send a message and stream lifecycle events via SSE

The `/send` endpoint streams agent events (`send_start`, `message_start`, `message_update`, `tool_start`, `tool_end`, `send_end`, etc.) as Server-Sent Events, enabling real-time UI updates.
24 changes: 24 additions & 0 deletions .changeset/public-places-occur.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
---
'@mastra/core': minor
---

Added `.send()` method to the Agent class — a higher-level orchestration method that wraps `stream()` with event emission, abort handling, and error recovery.

`.send()` emits lifecycle events (`send_start`, `send_end`), message events (`message_start`, `message_update`, `message_end`), tool events (`tool_start`, `tool_end`, `tool_approval_required`), and usage events (`usage_update`) so UIs can subscribe and react.

Also adds `abort()` to cancel in-progress sends and `respondToToolApproval()` for interactive tool approval flows.

```typescript
agent.subscribe(event => {
if (event.type === 'message_update') console.log(event.message);
});

const { message } = await agent.send({
messages: 'Hello!',
threadId: 'thread-1',
resourceId: 'user-1',
});

// Cancel:
agent.abort();
```
37 changes: 37 additions & 0 deletions .changeset/silly-ravens-strive.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
---
'@mastra/core': minor
---

Added optional orchestration capabilities to the Agent class: event system (subscribe/on), modes (named presets of instructions/model/tools), and shared state (Zod-validated). These are the foundation for folding Harness capabilities into Agent, reducing the number of concepts users need to learn.

**Events** - Subscribe to agent lifecycle events with `subscribe()` or `on()`:

```typescript
agent.on('mode_changed', event => {
console.log(`Switched to ${event.modeId}`);
});
```

**Modes** - Define named presets and switch between them at runtime:

```typescript
const agent = new Agent({
modes: [
{ id: 'plan', name: 'Plan', default: true, model: 'anthropic/claude-sonnet-4-20250514' },
{ id: 'build', name: 'Build', model: 'anthropic/claude-sonnet-4-20250514', tools: buildTools },
],
});

agent.switchMode('build');
```

**State** - Shared, Zod-validated state that persists across mode switches:

```typescript
const agent = new Agent({
stateSchema: z.object({ counter: z.number().default(0) }),
});

agent.setState({ counter: 1 });
console.log(agent.getState()); // { counter: 1 }
```
204 changes: 204 additions & 0 deletions docs/src/content/en/reference/agents/agent.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,27 @@ SystemMessage types: string | string[] | CoreSystemMessage | CoreSystemMessage[]
description:
'Zod schema for validating request context values. When provided, the context is validated at the start of generate() or stream(), throwing a MastraError if validation fails.',
},
{
name: 'harness',
type: 'AgentHarnessConfig',
isOptional: true,
description:
'Per-session orchestration config. Defines modes and state schema available per `.send()` operation. Values are NOT stored on the Agent singleton — they are passed per-operation for session isolation.',
properties: [
{
name: 'modes',
type: 'AgentMode[]',
isOptional: true,
description: 'Named presets. Select a mode per `.send()` via `modeId`.',
},
{
name: 'stateSchema',
type: 'z.ZodObject',
isOptional: true,
description: 'Zod schema for per-session state. Validated per `.send()` operation.',
},
],
},
]}
/>

Expand All @@ -237,6 +258,189 @@ SystemMessage types: string | string[] | CoreSystemMessage | CoreSystemMessage[]
]}
/>

## Methods

### Events

#### `subscribe(listener)`

Subscribe to all agent events. Returns an unsubscribe function.

```typescript
const unsub = agent.subscribe(event => {
console.log(event.type, event)
})

// later:
unsub()
```

#### `on(type, handler)`

Subscribe to a specific event type. Returns an unsubscribe function.

```typescript
agent.on('mode_changed', event => {
console.log(`Switched to mode: ${event.modeId}`)
})
```

### Harness

#### `hasModes()`

Returns `true` if the agent has modes configured in its `harness` config.

```typescript
if (agent.hasModes()) {
console.log('Default mode:', agent.getDefaultMode()?.id)
}
```

#### `listModes()`

Returns all configured modes from the harness config.

```typescript
const modes = agent.listModes()
```

**Returns:** `AgentMode[]`

#### `getDefaultMode()`

Returns the default mode (marked `default: true`, or the first mode).

```typescript
const defaultMode = agent.getDefaultMode()
```

**Returns:** `AgentMode | undefined`

#### `getStateSchema()`

Returns the Zod state schema from the harness config, if configured.

```typescript
const schema = agent.getStateSchema()
```

**Returns:** `z.ZodObject | undefined`

### Orchestration

#### `send({ messages, threadId, resourceId, ... })`

Send a message and get back a self-contained `SendOperation`. Each call has its own isolated event stream, abort handle, and tool-approval resolver — multiple concurrent sends to the same Agent do not interfere with each other.

Requires `memory` to be configured on the agent.

```typescript
const op = agent.send({
messages: 'Hello!',
threadId: 'thread-1',
resourceId: 'user-1',
modeId: 'plan',
state: { counter: 0 },
})

// Consume events as an async iterable
for await (const event of op.events) {
if (event.type === 'message_update') console.log(event.message.content)
}

// Or await the final message directly
const { message } = await op.result
```

<PropertiesTable
content={[
{
name: 'messages',
type: 'MessageListInput',
description: 'The message(s) to send. Can be a string or structured message input.',
},
{
name: 'threadId',
type: 'string',
description: 'The thread ID for memory persistence.',
},
{
name: 'resourceId',
type: 'string',
description: 'The resource ID for memory persistence.',
},
{
name: 'modeId',
type: 'string',
isOptional: true,
description:
'Mode to use for this operation. Must match an id from `harness.modes`. Falls back to the default mode.',
},
{
name: 'state',
type: 'Record<string, unknown>',
isOptional: true,
description:
'Per-operation state. Validated against `harness.stateSchema` if configured. Accessible by tools via `requestContext.get("agentState")`.',
},
{
name: 'maxSteps',
type: 'number',
isOptional: true,
defaultValue: '100',
description: 'Maximum number of LLM steps allowed.',
},
{
name: 'requestContext',
type: 'RequestContext',
isOptional: true,
description: 'Request context for dependency injection.',
},
{
name: 'toolsets',
type: 'ToolsetsInput',
isOptional: true,
description: 'Additional tool sets available during this send.',
},
{
name: 'abortSignal',
type: 'AbortSignal',
isOptional: true,
description: 'External abort signal. When aborted, cancels the stream.',
},
]}
/>

**Returns:** `SendOperation`

The returned `SendOperation` has the following properties:

<PropertiesTable
content={[
{
name: 'result',
type: 'Promise<{ message: AgentMessage }>',
description: 'Resolves when the send completes, with the assembled assistant message.',
},
{
name: 'events',
type: 'AsyncIterable<AgentEvent>',
description: 'Async iterable of events emitted during this send. Use with `for await`.',
},
{
name: 'abort()',
type: '() => void',
description: 'Abort this specific send operation.',
},
{
name: 'respondToToolApproval(decision, requestContext?)',
type: '(decision: "approve" | "decline", requestContext?: RequestContext) => void',
description: 'Respond to a tool approval request within this send operation.',
},
]}
/>

## Related

- [Agents overview](/docs/agents/overview)
28 changes: 28 additions & 0 deletions docs/src/content/en/reference/server/routes.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ Server adapters register these routes when you call `server.init()`. All routes
| `GET` | `/api/agents/:agentId` | Get agent by ID |
| `POST` | `/api/agents/:agentId/generate` | Generate agent response |
| `POST` | `/api/agents/:agentId/stream` | Stream agent response |
| `POST` | `/api/agents/:agentId/send` | Send message with event streaming (SSE) |
| `GET` | `/api/agents/:agentId/modes` | List available modes from harness config |
| `GET` | `/api/agents/:agentId/tools` | List agent tools |
| `POST` | `/api/agents/:agentId/tools/:toolId/execute` | Execute agent tool |

Expand Down Expand Up @@ -54,6 +56,32 @@ Server adapters register these routes when you call `server.init()`. All routes
}
```

### Send request body

```typescript prettier:false
{
messages: string | CoreMessage[]; // Required
threadId: string; // Required — thread for memory
resourceId: string; // Required — resource for memory
modeId?: string; // Mode to use for this request
maxSteps?: number; // Max tool steps (default: 100)
bodyRequestContext?: Record<string, unknown>; // Additional request context
}
```

The response is an SSE stream of agent events (`send_start`, `message_start`, `message_update`, `message_end`, `tool_start`, `tool_end`, `usage_update`, `error`, `send_end`).

Pass `modeId` to select which mode's instructions, model, and tools to use for this specific request. Each request is isolated — selecting a mode does not affect other concurrent requests.

### Modes response

```typescript prettier:false
{
modes: Array<{ id: string; name?: string; default?: boolean }>;
currentModeId?: string;
}
```

## Workflows

| Method | Path | Description |
Expand Down
Loading