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

# Extending the Managed Agent

`@telnyx/acp/managed` exports the classes the CLI generates for a configuration project. A code-based agent extends them.

| Export | What It Is |
| - | - |
| `defineManagedAgent(spec, host?)` | A class bound to the spec: the managed loop for texts and `invoke`. |
| `defineManagedVoiceAgent(spec, host?)` | The same, wrapped in the `voice()` mixin from [`@telnyx/edge-runtime/voice`](/docs/agent-sdk/runtime-sdk), which answers calls. |
| `ManagedAgent` | The loop itself, for a class that builds its own host in the constructor. |
| `ReplyVia`, `ManagedAgentSpec`, `ManagedAgentHost` | The types. |

Every class is a runtime SDK [`Agent`](/docs/agent-sdk/runtime-sdk), so [`this.messages`](/docs/agent-sdk/interface/messages), [`getState()`](/docs/agent-sdk/interface/getState) and [`setState()`](/docs/agent-sdk/interface/setState), [`queue()`](/docs/agent-sdk/interface/queue), [`schedule()`](/docs/agent-sdk/interface/schedule), [`@rpc`](/docs/agent-sdk/interface/rpc) methods and the rest of its API are there to use.

## The Host

The second argument is what `agent.json` cannot say:

```ts theme={null}
import { ManagedAgent, type ManagedAgentHost, type ManagedEnv } from "@telnyx/acp/managed";
import type { ActorContext } from "@telnyx/edge-runtime";
import { tool } from "ai";
import { z } from "zod";

const spec = { name: "support-agent", model: "zai-org/GLM-5.3-Flash", instructions: "…", approvals: { refund_order: "ask" } } as const;

const host: ManagedAgentHost = {
  tools: {
    refund_order: tool({
      description: "Refund an order",
      inputSchema: z.object({ orderId: z.string() }),
      execute: async ({ orderId }) => ({ refunded: orderId }),
    }),
  },
  authorization: myAuthorizationProvider,   // who may approve; without it the default allows every decision under the agent's own identity
  approvalTtlMs: 15 * 60 * 1000,            // how long a granted approval stays usable (5 min to 24 h)
};

export class SupportAgent extends ManagedAgent<ManagedEnv> {
  constructor(ctx: ActorContext, env: ManagedEnv) {
    super(ctx, env, spec, host);
  }
}
```

| Host Field | Meaning |
| - | - |
| `tools` | A tool set, in the `ai` package's shape. `spec.approvals` marks each tool. An `ask` tool pauses the run until `approve` or `deny`, and a `deny` tool is withheld. |
| `authorization` | Identity and policy for approvals, registered as the harness's [authorization port](/docs/agent-sdk/harness-sdk/ports/authorization). |
| `approvalTtlMs` | 5 minutes to 24 hours. |
| `model` | A language model used outright, instead of `spec.model` over the platform. |
| `completions` | Chat completions for `spec.model`, when the platform's endpoint is not the right one. |
| `environmentKeys` | Which bindings cross into the harness ports' [`environment`](/docs/agent-sdk/harness-sdk/ports/environment) (none by default). |
| `authorize(token, req)` | Who may read the streams and call `@rpc` methods over the mount. Everyone, unless the class narrows it. |

### Approvals, End to End

A run that reaches a tool marked `ask` pauses, and the state records the paused run as `awaitingApprovalRunId`; `invoke` ends with a line naming that run instead of waiting. Under `dev`, the line also carries the approval's id. A decision is one RPC on the conversation:

```bash theme={null}
telnyx-acp rpc support-agent --conversation alice inspectApproval <approvalId>
telnyx-acp rpc support-agent --conversation alice approve <approvalId>
telnyx-acp rpc support-agent --conversation alice deny <approvalId>
```

Under `dev`, approvals are authorized by a fixed local identity that allows every decision, and a paused run is printed with the `rpc … --local approve <id>` line to decide it. Deployed, a host that passes no `authorization` gets the loop's built-in default: every decision is allowed, under the agent's own identity (`agent:<name>`), so whoever the mount admits to `rpc` decides (everyone, in the beta phase). Pass `authorization` to require a real caller identity and policy; the harness itself refuses a run with an `ask` tool when no provider is registered at all.

## What to Override

```ts theme={null}
import { defineManagedAgent, type ReplyVia } from "@telnyx/acp/managed";
import { rpc } from "@telnyx/edge-runtime";

export class SupportAgent extends defineManagedAgent(spec) {
  /** Where a reply goes out. The default sends a text on the profile the message came in on. */
  protected override async deliver(text: string, via: ReplyVia): Promise<void> {
    await super.deliver(text, via);
    await this.queue("audit", { text });              // the class's own persistent follow-up
  }

  /** An RPC of the class's own, reachable over the mount and with `telnyx-acp rpc`. */
  @rpc({ description: "The last reply" })
  async lastReply(): Promise<string | undefined> {
    return (await this.getState()).lastReply;
  }

  protected override async onTask(name: string, payload: unknown, info: { attempt: number }): Promise<void> {
    if (name === "audit") return;                     // handle the class's own; everything else is the loop's
    await super.onTask(name, payload, info);
  }
}
```

The loop keeps its six `@rpc` methods, `receive`, `approve`, `deny`, `inspectApproval`, `pendingApproval` and `run`. Methods without `@rpc` are private over the mount, which is how the webhook handler's `receiveVia` (which names where a reply goes) is reachable in-process only.

## The Voice Hooks

`defineManagedVoiceAgent(spec)` answers calls. Its hooks, all overridable:

| Hook | When | Default |
| - | - | - |
| `greeting(call)` | the call is answered | `spec.voice.greeting`, or "Hi, this is \<name>. How can I help?" |
| `turnEngine()` | each turn | the direct engine (model + in-memory history); the harness with `voice.harness` |
| `reply(transcript, call)` | each reply | the mixin's reply wrapped in `withFiller`, which says one short phrase when the model is slow to start, tuned by `voice.filler` |
| `speechProviders()` | the call starts | STT and TTS, tuned from `spec.voice` |
| `turnEnded(metrics, call)` | a turn's playout ends | logs the turn's clock, writes the exchange |
| `callEnded(call, reason)` | hang-up | writes the last reply, ingests the call into memory |
| `warmCall(from?)` | before the call is answered | starts the instance, names the caller, begins the first turn's reading |

`call.from` and `call.to` are the numbers, `call.callControlId` the call. [The Voice Loop](/docs/agent-sdk/inside/turn-taking/voice-loop) shows how the class drives the mixin.

## Dropping to the SDK

A class that extends the runtime SDK's `Agent` directly, or `voice(Agent)`, is also a valid entry. The CLI deploys any exported agent class and wires the declared channels to it, and the loop is left to the user. The webhook handlers then call `receiveVia(text, from, key, via)` on that class for a text and open the media socket to it for a call. A class of the user's own implements them as the managed loop does, or as [Bring Your Own Framework](/docs/agent-sdk/examples/bring-your-own-framework) shows.
