Strands
Strands models a conversation differently from a flat message list. Its message role is
only ever user or assistant — there is no system role in the message list. The
system prompt is a separate argument to the agent. Message content is also a list of
content blocks rather than a bare string, so a text turn is { text: "..." }.
So the bridge partitions a Prompting Press composition into two parts:
- System prompt — every
system-role text, joined in order with a blank line, becomes the singlesystem_prompt/systemPromptargument (or nothing, if there is no system message). - Messages — every other turn maps to
{ role, content: [{ text }] }.
Split a composition for Strands
Section titled “Split a composition for Strands”"""Use Prompting Press prompts with the Strands Agents SDK (Python).
Strands has no ``system`` role in its message list: ``role`` is exactly``user`` | ``assistant``, and the system prompt is a SEPARATE ``Agent``argument. So the bridge PARTITIONS a Prompting Press composition:
* system-role texts -> a single ``system_prompt`` string (joined with "\\n\\n") * everything else -> ``messages`` as ``{role, content: [{"text": ...}]}``
Note: Strands cannot preserve the POSITION of a system message inside theconversation — every system-role text is hoisted into the one system prompt,regardless of where it sat. That is a Strands limitation, not somethingPrompting Press can carry across. Only plain text is mapped; Strands'provider-specific content blocks (guardContent, toolResult, ...) are out ofscope here. Standalone."""
from prompting_press import Composition, Promptfrom pydantic import BaseModelfrom strands import Agent
class TextVars(BaseModel): text: str
def to_strands(messages): """Partition a Prompting Press composition for Strands.
Returns ``(system_prompt, convo)`` where ``system_prompt`` is the "\\n\\n"-joined system-role texts (or ``None`` if there were none) and ``convo`` is the non-system messages as Strands content-block messages. """ system_texts = [m.text for m in messages if m.role == "system"] system_prompt = "\n\n".join(system_texts) if system_texts else None convo = [ {"role": m.role, "content": [{"text": m.text}]} for m in messages if m.role != "system" ] return system_prompt, convo
def _prompt(name, role, trusted=True): return Prompt( { "name": name, "role": role, "body": "{{ text }}", "variables": {"text": {"type": "string", "trusted": trusted}}, } )
# TWO system messages (so the "\n\n"-join + ordering is actually exercised),# then a user/assistant/user exchange.comp = Composition()comp.append(_prompt("sys-1", "system"), TextVars(text="You are a support agent."))comp.append(_prompt("sys-2", "system"), TextVars(text="Answer only in English."))comp.append( _prompt("u-1", "user", trusted=False), TextVars(text="What's your return policy?"))comp.append(_prompt("a-1", "assistant"), TextVars(text="30 days, unopened."))comp.append(_prompt("u-2", "user", trusted=False), TextVars(text="And opened items?"))
system_prompt, convo = to_strands(comp.resolve())
# Construct the agent from the two seams (no .run()/no model call — offline).agent = Agent(system_prompt=system_prompt, messages=convo)print(agent.system_prompt) # "You are a support agent.\n\nAnswer only in English."
# --- assertions (this file is executed by CI) ---
# Both system texts hoisted, joined in order with a blank line between.assert system_prompt == "You are a support agent.\n\nAnswer only in English."
# convo drops the system messages; only user/assistant remain, in order,# each wrapped as a single {"text": ...} content block.assert convo == [ {"role": "user", "content": [{"text": "What's your return policy?"}]}, {"role": "assistant", "content": [{"text": "30 days, unopened."}]}, {"role": "user", "content": [{"text": "And opened items?"}]},]
# The agent accepted both seams.assert agent.system_prompt == system_promptassert len(agent.messages) == 3assert [m["role"] for m in agent.messages] == ["user", "assistant", "user"]to_strands returns (system_prompt, convo). Pass them as the two agent arguments:
Agent(system_prompt=system_prompt, messages=convo). The example uses two system messages
to show the join; a real composition often has just one (or none).
// Use Prompting Press prompts with the Strands Agents SDK (TypeScript).//// Strands has no `system` role in its message list: `role` is exactly// `"user" | "assistant"`, and the system prompt is a SEPARATE Agent argument.// So the bridge PARTITIONS a Prompting Press composition://// * system-role texts -> a single `systemPrompt` string (joined with "\n\n")// * everything else -> `messages` as { role, content: [{ text }] }//// Strands cannot preserve the POSITION of a system message inside the// conversation — every system-role text is hoisted into the one system prompt.// That is a Strands limitation, not something Prompting Press can carry. Only// plain text is mapped; Strands' provider-specific content blocks// (guardContent, toolResult, ...) are out of scope here.import assert from "node:assert/strict";import { test } from "node:test";import { z } from "zod";import { Composition, Prompt } from "prompting-press";import { Agent } from "@strands-agents/sdk";
const TextVars = z.object({ text: z.string().min(1) });
type Msg = { role: string; text: string };
// Returns the separate system prompt (undefined if none) + the Strands messages.function toStrands(messages: Msg[]): { system: string | undefined; convo: { role: "user" | "assistant"; content: { text: string }[] }[];} { const systemTexts = messages.filter((m) => m.role === "system").map((m) => m.text); const system = systemTexts.length ? systemTexts.join("\n\n") : undefined; const convo = messages .filter((m) => m.role !== "system") .map((m) => ({ role: m.role as "user" | "assistant", content: [{ text: m.text }] })); return { system, convo };}
function prompt(name: string, role: "system" | "user" | "assistant", trusted = true) { return new Prompt({ name, role, body: "{{ text }}", variables: { text: { type: "string", trusted } }, });}
test("partition a Prompting Press composition for Strands", () => { // TWO system messages (exercises the "\n\n"-join + ordering), then a // user/assistant/user exchange. const comp = Composition.fromMessages([ { prompt: prompt("sys-1", "system"), schema: TextVars, data: { text: "You are a support agent." } }, { prompt: prompt("sys-2", "system"), schema: TextVars, data: { text: "Answer only in English." } }, { prompt: prompt("u-1", "user", false), schema: TextVars, data: { text: "What's your return policy?" } }, { prompt: prompt("a-1", "assistant"), schema: TextVars, data: { text: "30 days, unopened." } }, { prompt: prompt("u-2", "user", false), schema: TextVars, data: { text: "And opened items?" } }, ]);
const { system, convo } = toStrands(comp.resolve());
// Both system texts hoisted, joined in order with a blank line between. assert.equal(system, "You are a support agent.\n\nAnswer only in English.");
// convo drops the system messages; only user/assistant remain, in order, // each wrapped as a single { text } content block. assert.deepEqual(convo, [ { role: "user", content: [{ text: "What's your return policy?" }] }, { role: "assistant", content: [{ text: "30 days, unopened." }] }, { role: "user", content: [{ text: "And opened items?" }] }, ]);
// Construct the agent from the two seams (no invocation — offline). const agent = new Agent({ systemPrompt: system, messages: convo }); assert.equal(agent.systemPrompt, system); assert.equal(agent.messages.length, 3); assert.deepEqual( agent.messages.map((m) => m.role), ["user", "assistant", "user"], );});toStrands returns { system, convo }. Construct the agent with
new Agent({ systemPrompt: system, messages: convo }).
What’s not mapped
Section titled “What’s not mapped”The recipe maps plain text only. Strands’ provider-specific content blocks — guard content, tool-use and tool-result blocks, cache points, and the like — are request-assembly concerns that belong to your call layer, not to a rendered prompt. Prompting Press produces text; if you need those blocks, add them in your own Strands code around the mapped messages.
docs current as of 0.2.0