Starting in 0.25.0, Pika ships an optional Python converse Lambda built on the Strands Agents SDK as a drop-in replacement for the default TypeScript converse Lambda. This guide walks through when to use it, how to turn it on, and what changes under the hood.
What You'll Accomplish
Section titled “What You'll Accomplish”By the end of this guide, you will:
- Understand how the Strands path differs from the TypeScript converse Lambda
- Enable the opt-in
ConverseStrandsConstructviapika-config.ts - Swap your chat app's SSM lookup to route traffic through the Strands Lambda
- Configure your local and CI bundling environment (arm64 + QEMU)
- Verify the cutover in CloudWatch
Prerequisites
Section titled “Prerequisites”- Pika 0.25.0 or later
- A working Pika deployment on AWS (both
pikaservice stack andpika-chatstack) - Docker running locally for CDK Python Lambda bundling
- On x86_64 CI runners: QEMU + buildx available (see CI/CD Bundling)
When to Use the Strands Lambda
Section titled “When to Use the Strands Lambda”The Strands Lambda is a full-parity alternative to the TypeScript converse Lambda. It exists because the default path delegates the agent tool loop entirely to Bedrock via InvokeInlineAgentCommand, which limits what Pika can observe or override. Strands runs the loop itself, giving you:
- Fewer LLM calls — Semantic directives are modeled as Strands Skills instead of a separate Nova Lite selection call. Typical savings of one model invocation per request
- Iteration & budget control — Cap tool-call iterations, apply time budgets, and intercept individual tool calls
- Prompt caching —
CacheConfig(strategy="auto")on the Bedrock model (not available throughInvokeInlineAgentCommand) - Native streaming — Real-time trace, heartbeat, and
pika-metadataemission via a queue-based streaming loop
Early A/B benchmarks on representative workloads showed meaningful latency, cost, and tool-call reductions versus the TypeScript path. Actual numbers depend heavily on agent shape, tool count, and model mix — treat the Strands path as an opt-in experiment and run your own A/B before cutting production traffic over.
Architecture
Section titled “Architecture”The Strands Lambda is deployed as a framework-managed opt-in construct. When the feature flag is on, the pika service stack instantiates a ConverseStrandsConstruct alongside the TypeScript converse Lambda — both exist simultaneously, each publishing their own SSM parameter:
/stack/<proj>/<stage>/function/converse_url— TypeScript Lambda (default)/stack/<proj>/<stage>/function/converse_strands_url— Python/Strands Lambda
The chat app picks which URL to call by reading one of these SSM parameters in apps/pika-chat/infra/lib/stacks/pika-chat-construct.ts. Swapping between the two is a one-line change in that file.
┌──────────────────┐│ Chat App ││ (pika-chat) │└────────┬─────────┘ │ reads one SSM param at deploy time ▼ /function/converse_url ◀─── ConverseFn (TypeScript, default) /function/converse_strands_url ◀─── ConverseStrandsFunction (Python, opt-in)Runtime and Bundling
Section titled “Runtime and Bundling”- Runtime: Python 3.14
- Architecture: Graviton (arm64)
- Response streaming: AWS Lambda Web Adapter layer (
LambdaAdapterLayerArm64) - Container: FastAPI wrapper + queue-based streaming so Swarm's
stream_asynccan coexist with Lambda Web Adapter's event loop
Step 1: Enable the Feature Flag
Section titled “Step 1: Enable the Feature Flag”Edit pika-config.ts at the repository root:
export const pikaConfig: PikaConfig = { // ... siteFeatures: { // ... intentRouter: { enabled: true }, strandsConverse: { enabled: true } // ← add this }, // ...};This tells pika-stack.ts to instantiate the ConverseStrandsConstruct. The TypeScript converse Lambda is always created regardless — the flag only controls whether the Strands Lambda is also deployed.
Step 2: Route Traffic to the Strands Lambda
Section titled “Step 2: Route Traffic to the Strands Lambda”Edit apps/pika-chat/infra/lib/stacks/pika-chat-construct.ts and change the SSM parameter name that the chat app reads:
// Before (default — routes to TypeScript converse Lambda)const converseFnUrl = ssm.StringParameter.valueForStringParameter( this, `/stack/${props.pikaServiceProjNameKebabCase}/${props.stage}/function/converse_url`);
// After (routes to Strands Python Lambda)const converseFnUrl = ssm.StringParameter.valueForStringParameter( this, `/stack/${props.pikaServiceProjNameKebabCase}/${props.stage}/function/converse_strands_url`);Step 3: Configure Bundling
Section titled “Step 3: Configure Bundling”Local Development
Section titled “Local Development”CDK uses Docker to build the Python Lambda for linux/arm64. On an Apple Silicon Mac, this runs natively. On Intel/AMD machines, Docker Desktop handles emulation transparently.
Verify Docker is running before cdk synth or cdk deploy:
docker infoCI/CD Bundling (arm64 + QEMU)
Section titled “CI/CD Bundling (arm64 + QEMU)”Standard x86_64 GitHub Actions runners cannot execute arm64 Docker images without emulation. Add QEMU + Buildx to your deploy workflow before the CDK deploy step:
# .github/workflows/deploy.yml (or equivalent)- name: Set up QEMU uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx uses: docker/setup-buildx-action@v3
- name: CDK deploy run: pnpm --filter @pika/service cdk:deployBundling the Python Lambda under QEMU takes roughly 60–120 seconds per build. If that becomes a bottleneck, provision native arm64 runners instead of emulating — both approaches work; this guide doesn't prescribe one.
Step 4: Local Development Tooling
Section titled “Step 4: Local Development Tooling”The Strands Lambda ships with helper scripts in services/pika/package.json:
| Command | What it does |
|---|---|
pnpm --filter @pika/service strands:setup | Creates a .venv, installs Python + dev dependencies |
pnpm --filter @pika/service strands:test | Runs the Python pytest suite (contract + unit tests) |
pnpm --filter @pika/service strands:invoke | Invokes the handler locally against real DDB + Bedrock |
pnpm --filter @pika/service strands:validate | Full loop: setup, test, CDK synth |
Run strands:setup once per clone, then strands:test on each change. strands:invoke requires AWS credentials and an agent definition already seeded in DynamoDB.
Local Tool Routing with LOCAL_TOOLS
Section titled “Local Tool Routing with LOCAL_TOOLS”By default each tool is invoked on its deployed Lambda. Set the LOCAL_TOOLS environment variable to send selected tools to a locally running Lambda emulator instead.
LOCAL_TOOLS accepts either inline JSON or the path of a JSON file. A value that does not parse as JSON is treated as a file path, resolved against the working directory. The JSON is an object keyed by tool id or by the function name taken from the tool's Lambda ARN. For a qualified ARN (...:function:name:alias) the function name is name, without the alias.
Each value is one of:
- An endpoint URL string.
- An object
{ "endpoint": "<url>", "function": "<name>" }.functionis optional and replaces the function name derived from the ARN.
{ "my-tool-id": "http://localhost:9001", "my-app-prod-other-tool": { "endpoint": "http://localhost:9001", "function": "my-app-test-other-tool" }}A routed tool is invoked through a Lambda client pointed at the endpoint, addressed by bare function name rather than ARN. Tools with no matching entry stay on their deployed Lambda. Entries that are neither a URL string nor an object with a non-empty endpoint are skipped with a warning.
If the file is missing, unreadable or not valid JSON, or the JSON is not an object, LOCAL_TOOLS is ignored with a warning and every tool stays on its deployed Lambda.
LOCAL_TOOLS is re-read on every tool call, so edits to the file take effect without restarting. A transient read failure sends that one call to the deployed Lambda.
Pinning the Session with SESSION_ID
Section titled “Pinning the Session with SESSION_ID”local_invoke.py (strands:invoke) uses strands-test-<epoch seconds> as the session id. Set the SESSION_ID environment variable to use a fixed session id instead, so repeated runs continue the same conversation.
Step 5: Deploy and Verify
Section titled “Step 5: Deploy and Verify”Deploy the service stack first (creates the Lambda + SSM params), then the chat stack (reads the param you flipped):
pnpm --filter @pika/service cdk:deploypnpm --filter @pika/chat cdk:deployAfter deploy, verify:
Lambda exists and INIT succeeds — check
/aws/lambda/<project>-<stage>-ConverseStrandsFunction...in CloudWatch Logs. You should seeUvicorn running on http://0.0.0.0:8080. If you seeModuleNotFoundError, the bundling platform mismatched the runtime arch (see the caution box above).Traffic is flowing to Strands — send a chat message and tail the same log group. You should see the request handler entry and subsequent streaming traces. If you see no traffic, confirm the chat stack was deployed after flipping the SSM path.
SSM parameters are correct:
aws ssm get-parameter --name /stack/<proj>/<stage>/function/converse_strands_urlaws ssm get-parameter --name /stack/<proj>/<stage>/function/converse_arnRolling Back
Section titled “Rolling Back”The TypeScript Lambda stays deployed while the Strands flag is on. To roll back:
- Change the SSM lookup in
pika-chat-construct.tsback toconverse_url - Redeploy the chat stack
Traffic cuts back to the TypeScript path immediately. You can leave strandsConverse.enabled = true if you want to keep the Strands Lambda warm for experimentation — or flip it off to stop paying for an idle function.
Migrating from an Existing Strands Customization
Section titled “Migrating from an Existing Strands Customization”If you previously added a Strands-style Lambda via services/pika/lib/stacks/custom-stack-defs.ts (customer override), migrate to the framework construct after upgrading to 0.25.0:
- Delete your custom
createStrandsConverseLambda()implementation and any supporting constants fromcustom-stack-defs.ts. Restore the file to the pika scaffold defaults. - Flip
pikaConfig.siteFeatures.strandsConverse.enabledtotrue. - Deploy.
CloudFormation will replace the old resources (deleting the customer-side logical IDs and creating new framework-side ones). The Function URL will change, but the /function/converse_strands_url SSM parameter rewrites automatically — downstream consumers picking it up through SSM will cut over on their next deploy. Expect a brief reachability gap at cutover; coordinate an off-hours deploy if needed.
What's Next
Section titled “What's Next”- Compare against the TypeScript path in a non-prod stage. Deploy both Lambdas side-by-side, then alternate the SSM lookup in
pika-chat-construct.tsto A/B them. CloudWatch metrics (Duration, Invocations, Throttles) on the two function names give you the comparison data. - Review the Strands Agents SDK documentation. Customization the framework doesn't expose — tool loops, reasoning traces, custom callbacks — lives at the SDK layer. Start with strands-agents/sdk-python.
- Understand the directive pathway. The Strands path prefers Strands Skills for directive selection (no LLM call) and falls back to the same Nova Lite filtering the TS Lambda uses when Skills aren't viable. If you use semantic directives heavily, the Instruction Augmentation guide still applies — directive authoring and DDB storage are unchanged.
- Keep customizations out of
handler.py. Constants likeMAX_ITERATIONSandLAMBDA_TIMEOUT_BUFFER_SECONDSlive in framework code thatpika syncwill overwrite. If you need to tune them, open an issue on the Pika repo so the knob becomes configuration rather than a fork of the Lambda source.