# Building Telegram Agents with Amazon Bedrock AgentCore

## What This Is
Amazon Bedrock AgentCore provides managed container runtimes for AI agents that need long-running compute, session affinity, and background processing. It spins up a microVM per session, keeps it warm across messages from the same user, and shuts it down after an idle timeout. This skill covers using AgentCore to build a Telegram bot that invokes Claude to make code changes, with an async entrypoint pattern that returns fast while the agent works in the background.

## Key Concepts
- **AgentCore Runtime**: A container-based compute environment managed by Bedrock. You provide a Docker image with your agent code, and AgentCore handles provisioning, scaling, and lifecycle.
- **Runtime Session**: Each unique `runtimeSessionId` gets its own dedicated microVM. The session persists (workspace, memory, processes) until idle timeout or explicit termination.
- **Session Affinity**: Route subsequent requests from the same user to the same container by reusing the same `runtimeSessionId`. Derive it deterministically from the user identifier.
- **Async Entrypoint Pattern**: The `@app.entrypoint` handler returns immediately after spawning background work. Use `add_async_task` to keep the container alive and `complete_async_task` when done.
- **Idle Timeout**: Configurable via `idleRuntimeSessionTimeout` (default 900s / 15 min, range 60-28800s). The microVM terminates after this period of inactivity.
- **`from_asset()` in CDK**: Builds the Docker image from a local directory, pushes to ECR, and wires it into the Runtime resource automatically. No separate build pipeline needed.

## Setup

### Prerequisites
- Python 3.11+
- AWS CDK CLI (`npm install -g aws-cdk`)
- Docker or Finch
- AWS credentials with Bedrock AgentCore permissions
- A Telegram bot token from @BotFather

### Project Structure
```
app/
  my_agent/
    Dockerfile
    main.py          # AgentCore entrypoint
    requirements.txt
    lib/
      agent.py       # Agent logic
      auth.py        # Authorization
functions/
  telegram_webhook.py  # Lambda webhook relay
infra/
  stack.py             # CDK stack
```

### Agent Dependencies (requirements.txt)
```
bedrock-agentcore>=1.0.0
claude-agent-sdk>=0.1.0
boto3>=1.35.0
httpx>=0.27.0
```

## Common Patterns

### Pattern 1: Async Entrypoint with Background Thread

When to use: Any agent that needs more than a few seconds to complete its work. The entrypoint returns immediately so the caller (Lambda, API) gets a fast response.

```python
from bedrock_agentcore.runtime import BedrockAgentCoreApp
import threading

app = BedrockAgentCoreApp()

@app.entrypoint
def handle_request(payload, context=None):
    # Parse the incoming request
    user_id = payload["user_id"]
    message = payload["message"]

    # Register an async task so the container stays alive
    task_id = app.add_async_task(
        f"task-{user_id}",
        {"user_id": user_id},
    )

    # Spawn background work
    threading.Thread(
        target=do_work,
        args=(user_id, message, task_id),
        daemon=True,
    ).start()

    # Return immediately
    return {"status": "accepted", "task_id": task_id}

def do_work(user_id, message, task_id):
    try:
        # Your long-running agent logic here
        result = run_agent(user_id, message)
        send_response(user_id, result)
    finally:
        # Always complete the task so the container can go idle
        app.complete_async_task(task_id)
```

`add_async_task` tells AgentCore the container is busy (reports HealthyBusy on /ping). `complete_async_task` lets it go idle and eventually terminate. Always call `complete_async_task` in a `finally` block.

### Pattern 2: Session Affinity via Stable Session IDs

When to use: When you need workspace, conversation history, or installed dependencies to persist across multiple requests from the same user.

```python
# In the Lambda webhook relay
user_id = str(payload.get("message", {}).get("from", {}).get("id", "unknown"))
session_id = f"telegram-user-session-id-{user_id}"

# AgentCore requires session IDs to be at least 33 characters
client.invoke_agent_runtime(
    agentRuntimeArn=AGENT_RUNTIME_ARN,
    runtimeSessionId=session_id,
    contentType="application/json",
    accept="application/json",
    payload=json.dumps(payload).encode("utf-8"),
)
```

The same `runtimeSessionId` always routes to the same microVM. The workspace (git repos, node_modules, temp files) survives between requests. Conversation history stored in-memory persists as long as the session is alive.

### Pattern 3: CDK from_asset() for AgentCore Runtime

When to use: Deploying an AgentCore Runtime with CDK. Builds the Docker image from source and handles ECR automatically.

```python
import aws_cdk.aws_bedrock_agentcore_alpha as agentcore

agent_runtime = agentcore.Runtime(
    self, "MyRuntime",
    runtime_name="my_agent",
    agent_runtime_artifact=agentcore.AgentRuntimeArtifact.from_asset("app/my_agent/"),
    protocol_configuration=agentcore.ProtocolType.HTTP,
    lifecycle_configuration=agentcore.LifecycleConfiguration(
        idle_runtime_session_timeout=cdk.Duration.minutes(15),
    ),
    environment_variables={
        "MY_VAR": "value",
    },
)
```

CDK builds the Dockerfile in the specified directory, pushes the image to a managed ECR repo, and creates the Runtime resource. No manual image tagging or ECR commands.

### Pattern 4: Invoking Claude Agent SDK

When to use: Running Claude as a headless coding agent inside the AgentCore container.

```python
from claude_agent_sdk import query, ClaudeAgentOptions

async for message in query(
    prompt=user_message,
    options=ClaudeAgentOptions(
        system_prompt=system_prompt,
        allowed_tools=["Read", "Write", "Edit", "Bash", "Glob", "Grep"],
        cwd=workspace_path,
        max_turns=30,
    ),
):
    if hasattr(message, "result"):
        result_text = str(message.result)
```

The system prompt is where you define guardrails (which files the agent can modify, validation requirements, deployment workflow). `allowed_tools` controls what Claude can do. `cwd` sets the working directory. `max_turns` caps the number of tool-use cycles.

## API Reference

### BedrockAgentCoreApp
- `@app.entrypoint` — Decorator that registers the request handler. Receives `(payload: dict, context=None)`. Return value is sent back to the caller.
- `app.add_async_task(task_name: str, metadata: dict) -> task_id` — Registers a background task. Container reports HealthyBusy until completed.
- `app.complete_async_task(task_id)` — Marks a background task as done. Container can go idle.

### boto3 bedrock-agentcore client
- `invoke_agent_runtime(agentRuntimeArn, runtimeSessionId, contentType, accept, payload)` — Sends a request to an AgentCore Runtime. Returns the response body.

### Claude Agent SDK
- `query(prompt, options) -> AsyncIterator` — Invokes Claude with tools. Yields messages including tool calls and results.
- `ClaudeAgentOptions(system_prompt, allowed_tools, cwd, max_turns)` — Configuration for the Claude invocation.

## Pitfalls

- **Conversation history is in-memory.** It persists across messages within the same session because the container stays alive. But if the container recycles (idle timeout, crash, explicit stop), the history is gone. For durable history, persist to DynamoDB or S3.
- **Session IDs must be at least 33 characters.** AgentCore rejects shorter session IDs. Pad with a prefix if needed.
- **One request at a time per user.** If the agent uses a per-user git workspace, concurrent requests from the same user will conflict. Track active users and reject concurrent requests.

## Working Examples

### Minimal AgentCore Entrypoint
```python
from bedrock_agentcore.runtime import BedrockAgentCoreApp
import threading

app = BedrockAgentCoreApp()

@app.entrypoint
def handle_request(payload, context=None):
    task_id = app.add_async_task("work", {})

    def background():
        try:
            # Do work here
            pass
        finally:
            app.complete_async_task(task_id)

    threading.Thread(target=background, daemon=True).start()
    return {"status": "accepted", "task_id": task_id}
```

### Lambda Webhook Relay
```python
import json, os, boto3

AGENT_RUNTIME_ARN = os.environ["AGENT_RUNTIME_ARN"]

def handler(event, context):
    body = event.get("body", "{}")
    payload = json.loads(body) if isinstance(body, str) else body
    user_id = str(payload.get("message", {}).get("from", {}).get("id", "unknown"))

    client = boto3.client("bedrock-agentcore")
    client.invoke_agent_runtime(
        agentRuntimeArn=AGENT_RUNTIME_ARN,
        runtimeSessionId=f"telegram-user-session-id-{user_id}",
        contentType="application/json",
        accept="application/json",
        payload=json.dumps(payload).encode("utf-8"),
    )
    return {"statusCode": 200, "body": json.dumps({"ok": True})}
```


## Related Resources

- **Kiro `aws-agentcore` power**: Official Kiro power for Amazon Bedrock AgentCore. Covers the full AgentCore API, runtime management, and deployment workflows. Install from the Kiro Powers panel. This skill focuses specifically on the Telegram bot + async entrypoint + Claude SDK pattern that the power doesn't cover in detail.
