---
title: Managed Agents
description: A walking tour of what happens when a managed agent runs — runtime selection, context assembly, Bedrock invocation, tool execution, and streaming back — plus why managed does not mean vendor-hosted.
---

import { Aside } from '@astrojs/starlight/components';

Managed agents are the default ThinkWork runtime path — the harness's Planning + Action layer in [PPAF](/architecture/#the-ppaf-agent-loop). ThinkWork runs the agent loop for you inside your AWS account on **Bedrock AgentCore**. Strands is the stable default runtime; Pi is a second managed runtime that can be selected per agent or template. The runtime path gives you consistent thread integration, streaming, and auditability without you building any of that yourself, and it's the load-bearing implementation of two operating guarantees: **Reliability** (every turn resolves agent + template state freshly, so behavior is reproducible and a config rotation propagates without a deploy) and **Security** (Bedrock guardrails attach at model-invocation time so the boundary is non-bypassable, and tool capability grants are template-pinned so a compromised skill pack can't widen the agent's reach).

The important distinction is that **managed does not mean vendor-hosted**. ThinkWork makes running an agent easy, but the Lambda container is in your account, the thread state is in your Aurora cluster, the model invocation goes to your Bedrock endpoint, and the audit log lives in your S3 bucket. Nothing phones home; nothing depends on a ThinkWork SaaS control plane.

## A walking tour of one invocation

From "user sends a message" to "agent response streams back," end to end.

### 1 — The trigger

A new message lands in a thread assigned to a managed agent. How it got there doesn't matter — could be a chat from the mobile app, an inbound Slack event mapped by the Slack integration, or an automation's scheduled run. The message row is written to Aurora; an AppSync mutation fires; a message also lands on an SQS queue that the AgentCore runtime consumes.

### 2 — Runtime dispatch

The dispatcher resolves the assigned agent and template, reads the runtime selector, and invokes the matching AgentCore runtime function. If the selector is missing or unrecognized, the dispatcher uses Strands. If the selector is `pi` but the Pi function is not provisioned in the stage, the dispatcher returns a structured error instead of silently falling back.

See [Runtime Selection](/concepts/agents/runtime-selection/) for the selection model.

### 3 — Container invocation

The selected AgentCore runtime receives the invocation. If this is the first invocation in a while, the container cold starts — including pulling the image from ECR, initializing the runtime loop, and opening any needed clients. Warm invocations start much faster.

The Strands container is Python-based and preloaded with:

- Python 3.12
- Strands agent framework
- Boto3 (Bedrock, S3, SSM, Secrets Manager clients)
- httpx (for skill HTTP tools and MCP)
- psycopg3 (Aurora connection)
- The ThinkWork runtime library — tool registration, memory read/write, context assembly, OTel instrumentation

The Pi container is Node.js-based and runs the Pi loop while preserving the same invocation envelope and response contract. Both runtime images are built and deployed by CI.

Lambda memory is configured at deployment time.

### 4 — Agent and template resolution

The runtime receives the normalized agent and template configuration. From the agent row it gets the template id, system prompt, assigned skill packs, assigned knowledge bases, and runtime selector. From the template row it gets the model id, guardrail id, inference params, tool config, and default runtime.

Agent state is resolved *every invocation*. That matters: if an operator updated the template's `modelId` five seconds ago, this invocation already sees the new model. There is no cache that could serve stale config.

### 5 — Context assembly

Before calling the model, AgentCore assembles the context. Four sources feed in:

- **Thread history.** The most recent turns from the thread, windowed to fit alongside everything else.
- **Document retrieval.** If the agent has knowledge bases attached, each gets queried for the top-K most-similar chunks given the user's message. Results are merged.
- **Long-term memory recall.** If the agent has long-term memory configured, the memory adapter is queried for relevant prior facts — this is Hindsight or AgentCore Memory, depending on the deployment.
- **Compounding-memory wiki.** If the agent has a wiki, the recall tool surfaces relevant pages. Often this is faster and more precise than raw memory search because the pages are pre-organized.

All four pieces get stitched into the structured input that goes to Bedrock's `converse` API. The system prompt comes from the template base + the agent-specific prompt; the user message is the latest message in the thread; the retrieved content is added as tool context or `<knowledge>` blocks.

### 6 — Bedrock invocation

AgentCore calls `bedrock-runtime.converse` with the assembled context, the configured model id, and — critically — the configured `guardrailIdentifier`. The guardrail evaluator runs *inside* the Bedrock call, not as a wrapper around it. That's what makes the boundary non-bypassable.

The model returns either a direct response or a tool-use request.

### 7 — The runtime loop

If the model requested a tool, the selected runtime orchestrates the handshake:

1. The runtime looks up the requested tool in its registry (built-in + skill-pack-provided + MCP-connected).
2. The tool is invoked — could be a local Python function, an HTTP call to an external API, or an MCP call to a remote server.
3. The tool's output is added to the conversation and sent back to the model.
4. The model may request more tools, or produce a final response.

OpenTelemetry spans wrap every tool invocation and every model call, capturing input tokens, output tokens, and latency. Those spans become the raw material for per-turn cost capture downstream.

### 8 — Response and streaming

When the model produces its final response, AgentCore:

1. Writes a new `thread_message` row with role=`assistant`.
2. Writes a `thread_turn` row with the aggregated turn data (tool calls, memory reads, cost, duration).
3. Publishes an AppSync mutation so subscribed clients see the new message live.
4. If streaming is enabled, chunk-by-chunk updates stream via AppSync throughout step 6; the final message is just the completion marker.

### 9 — Post-turn side effects

A few things happen after the turn completes but before the Lambda returns:

- **Memory retention.** If AgentCore Memory is the adapter, a retention event fires so background strategies (summaries, preferences, semantic facts) can extract what's worth remembering.
- **Company Brain page compile enqueue.** If compiled pages are enabled, a compile job gets queued on a 5-minute dedupe bucket (see [Company Brain Compile Pipeline](/concepts/knowledge/compounding-memory-pipeline/)).
- **Audit record.** An NDJSON record of the full invocation lands in the audit S3 bucket.
- **Cost reducer.** The OTel spans emit; the cost reducer picks them up off the CloudWatch stream and writes a `thread_turn_costs` row.

Then the Lambda returns. Total wall time for a warm invocation with a typical context: 2–10 seconds depending on model + tool calls.

## Why this shape matters

A few things about this model that aren't obvious until you've operated it for a while:

- **Agent state resolution every turn.** No stale config. No deploy needed to rotate a model.
- **Tool config lives in the template.** A skill pack upload doesn't change what an agent can do; the template's `toolConfig` governs that. This matters for security — an attacker who somehow uploads a malicious skill pack still needs the template's tool flag to be on.
- **Every turn is observable twice.** From the thread side (narrative timeline) and from the execution side (OTel spans + audit record). Debugging is pleasant because the data is there.
- **Cold starts are visible.** A 3–5s cold start on the first turn after a quiet period is normal and expected. Set user expectations accordingly.

## What a managed agent includes (reference)

Every managed agent has:

- A **model** chosen through its template.
- A **runtime** selected on the agent or inherited from the template (`strands` by default, `pi` when explicitly selected).
- A **system prompt** stored on the agent record.
- Optional **skill packs** — the agent's capabilities.
- Optional **knowledge bases** — what it retrieves from.
- Optional **long-term memory** — the adapter behavior is configured on the template.
- Optional **compiled Company Brain pages** — derived context on top of long-term memory.
- **Control settings** inherited from the template — guardrails, rate limits, budgets.

<Aside type="note">
  Company Brain is the umbrella context layer in ThinkWork. Knowledge bases, memory, compiled pages, workspace files, and approved tools are different inputs into that layer — all can feed the same turn, and the harness handles assembly without the agent's author wiring it up.
</Aside>

## When to use managed agents

Managed agents are the best fit when you want:

- Bedrock-native deployment in your own AWS account.
- A standard runtime across many agents.
- Tight integration with threads, memory, automations, and controls.
- Simple operations for internal tools and production workflows.
- The operational benefits — guardrail integration, per-turn cost, fleet rotation, audit — that the managed path bakes in.

Use [connected agents](/concepts/agents/) instead when you already have an external runtime you want ThinkWork to orchestrate around rather than replace.

## How to configure it

Managed agents are configured at three layers — the agent record, the template the agent references, and the deployment-level Lambda settings. The split matters: most operator-side knobs live on the template (so they propagate fleet-wide), only a few are agent-scoped, and a small set of cross-cutting infrastructure dials are deployment-scoped (Terraform).

### Agent-scoped (admin web)

Per-agent settings at [Admin: Agents](/applications/admin/agents/):

- **System prompt.** The agent-specific instructions, written into the `system` block on every Bedrock call. This is the most-touched per-agent dial; templates seed it but agents typically diverge over time.
- **Runtime.** Which AgentCore runtime executes the next turn. Strands is the stable default; Pi is the parallel runtime substrate. Changing the runtime affects new turns, not turns already in flight.
- **Skill packs assigned.** Which capability bundles this specific agent can call. Seeded from the template at agent creation; editable per-agent thereafter (see [Templates: Inheritance and overrides](/concepts/agents/templates/#inheritance-and-overrides)).
- **Knowledge bases assigned.** Same shape — seeded, then per-agent editable.
- **Status.** `created / active / suspended / archived`. Suspension is usually automatic (budget reconciler hits a cap), but operators can flip status manually.
- **Compounding-memory wiki.** Whether this specific agent participates in the wiki pipeline. Off by default; turn on per-agent for assistants whose memory benefits from cross-thread pages.

### Template-scoped (admin web, propagates fleet-wide)

The configuration layer where most production work happens. See [Templates](/concepts/agents/templates/) for the full surface; the dials that matter for the managed runtime:

- **Model id.** Which Bedrock model the runtime calls. Changing this rotates the model on every agent that references the template — no deploy.
- **Default runtime.** Which AgentCore runtime newly-created agents from this template use unless overridden per agent.
- **Guardrail id + version.** The Bedrock Guardrail attached at the model invocation. Non-bypassable.
- **Inference parameters.** Max output tokens, temperature, top-p, and any other Bedrock `converse` params.
- **Tool config.** Which tool categories the agent's runtime registry should include — SQL, S3, web search, MCP-connected tools, built-in tools. Disabled categories are absent from the registry, not gated by prompt.
- **Memory adapter + recall config.** Hindsight or AgentCore Memory; how many recalls per turn; which memory strategies run.
- **Per-turn tool-call cap.** The safety valve against runaway tool loops.
- **Budget cap.** Monthly spend limit; whether hitting it auto-suspends agents on the template.

### Deployment-scoped (Terraform / CLI)

- **Lambda memory.** 3GB by default (≈ 2 vCPUs); raise via the AgentCore Lambda config when consistently CPU-bound.
- **Provisioned concurrency.** Off by default; set per-stage if cold-start latency on the first turn after a quiet period is unacceptable for your interactive paths.
- **VPC config.** AgentCore runs in your VPC by default. Subnets, security groups, NAT egress all configured in the foundation Terraform module.
- **Environment variables.** Bedrock endpoint, Aurora connection params, KMS keys, audit S3 bucket — all wired via Terraform; the runtime resolves them at boot.
- **Container image versions.** ThinkWork ships Strands and Pi as versioned AgentCore container images; deployments pin to specific tags. Upgrading is a `thinkwork deploy` step, not an admin-web setting.

### When the defaults are wrong

The defaults — 3GB Lambda, no provisioned concurrency, no VPC overrides — are calibrated for a single-tenant pilot with bursty traffic. They deviate as you scale:

- A high-RPS production agent benefits from provisioned concurrency on the AgentCore Lambda; cold-start cost amortizes badly when every minute has multiple cold starts.
- A long-running tool-heavy agent (think research routines that fan out 30+ tool calls) benefits from larger Lambda memory; the 15-minute Lambda max execution time is a hard ceiling either way.
- A regulated tenant may want VPC subnets without NAT egress, with a Bedrock VPC endpoint instead — keeps inference traffic inside the VPC boundary.

For all three, the configuration knobs above are sufficient — none of them require a code change.

## Known limits

- **Lambda max execution time is 15 minutes.** A single turn can't run longer than that. For longer work, use a routine with multiple steps.
- **Max container image size is 10GB.** Not a practical limit for typical skill packs, but very large reference corpora shouldn't ride in the image.
- **Cold starts add 3–5s to the first turn.** For latency-sensitive internal tools, keep Lambda warmed or route through a provisioned-concurrency configuration.
- **No per-invocation model override.** The template's model id is authoritative; per-turn model switching would require a different template assignment.
- **Runtime changes do not rewrite history.** Existing threads, memories, and audit records stay attached to the same agent; only subsequent invocations use the new runtime.

## Related pages

- [Agents (overview)](/concepts/agents/) — managed vs. connected
- [Runtime Selection](/concepts/agents/runtime-selection/) — Strands vs. Pi routing
- [Templates](/concepts/agents/templates/) — the live-configuration reuse layer (model, guardrail, budget)
- [Skills](/concepts/agents/skills/) — the capability reuse layer (tools + instructions)
- [Company Brain](/concepts/knowledge/) — the context layer managed agents read
- [Control](/concepts/control/) — how guardrails, budgets, and audit wrap the managed path
- [Architecture](/architecture/) — where AgentCore fits in the deployment

## Under the hood

The managed runtimes live in two sibling packages:

- `packages/agentcore-strands/agent-container/` — Python container built on the Strands agent framework.
- `packages/agentcore-pi/agent-container/` — Node.js container built around the Pi loop.

Both are deployed as AgentCore container images. The API dispatcher reads the agent/template runtime selector, chooses the runtime function, and passes the normalized invocation envelope through. The selected runtime assembles or consumes context from the memory + knowledge layers, calls Bedrock with the template's guardrail applied, orchestrates tool use, and writes turns + messages back to the thread.

For the code path, start in `packages/agentcore/` for the runtime itself and `packages/api/src/lib/memory/` for the memory adapter contract the runtime calls. Template and agent row shapes are defined in the operational database schema (`packages/database-pg/drizzle/`).
