Skip to main content

welt-io-strands

pypi python strands-agents

The Strands Agents (Python) adapter for Welt's wire contract.

Install

uv add welt-io-strands

Usage

welt_agent builds the whole AgentCore Runtime entrypoint for an agent Welt drives, so a deployable is your agent plus one mount line:

from bedrock_agentcore.runtime import BedrockAgentCoreApp
from strands import Agent
from welt_io_strands.agentcore import welt_agent

app = BedrockAgentCoreApp()
app.entrypoint(welt_agent(lambda: Agent(callback_handler=None)))

if __name__ == "__main__":
    app.run()

See examples/agent for the full version — the smallest complete agent built on this package (text streaming, tool use, image generation, file output, file input, and human-approval tools), which doubles as the example for Welt's Quick Start. The sections below cover the entrypoint and the adapters it wires in.

Supported Versions

Welt

While both are 0.x, a welt-io-strands 0.Y release supports Welt v0.Y. From 1.0 on, a release supports any Welt release that shares its major version, and the minor versions move independently. Support is best effort either way, and other combinations come with no guarantee.

Strands Agents

The badge at the top states the range this release installs against. Every push and pull request runs the suite at both ends of it: the declared floor, and the newest release CI has picked up. That is best effort rather than a guarantee — the floor is where the suite was last seen to pass, so a later release may raise it, and no ceiling is declared at all.

The badge follows the current release. For the range an older release declared, read that release's own metadata on PyPI.

Something misbehaving inside that range is worth an issue.

API

The wire between Welt and the agent is JSON, specified by Welt's wire contract — plain Strands values do not fit it in either direction. Two functions adapt the inbound payload, two the outbound stream. welt_agent wires the three of them the entrypoint needs (interrupt_reason serves the tools themselves); reach for the pieces directly when your entrypoint needs a shape of its own.

Entrypoint

welt_agent(new_agent, files_from=...)

Builds the entrypoint BedrockAgentCoreApp serves. It reads which envelope Welt sent — Converse-shaped messages for a conversation turn, interrupt_responses for the answers that resume an interrupted run — drives the agent, and yields the events Welt renders.

Every turn runs on a fresh Agent from new_agent: the Slack thread is the source of truth for conversation history, and the messages Welt sends carry it whole. An interrupted Agent waits inside the entrypoint for its answers — one slot, resume-only, living and dying with the session's microVM (recycled on idle timeout, 8 hours at most); resuming after that raises, which Welt renders as its resume-failure notice. files_from passes through to renderable_events below.

send_file(name, data)

Queues one file for the Slack thread from inside a tool, riding the wire beside the reply being streamed. The model never sees it — a tool whose file matters to the conversation says what it holds in its result, or returns it as a content block and is named in files_from, which puts it in front of the model and on the thread both. Every turn starts with the queue empty, so a file a failed turn left behind never rides a later reply, and an empty name or empty bytes is refused where the tool is still on the stack — Slack refuses a zero-byte upload, and the whole reply fails with it.

Inbound

decode_messages(messages)

Returns a copy of Welt's Converse-shaped messages with the base64-encoded file bytes restored to the raw bytes Strands expects; everything else — the format token included — is carried over untouched, and the input is left alone.

decode_interrupt_responses(responses)

Turns Welt's resume payload — a mapping of interrupt id to the answer a human chose and the widget it came from — into the interruptResponse items that Agent.stream_async resumes from. The answer travels on as the value it was given; the widget it came from is Welt's vocabulary, and a tool that reads its own option values already knows which of them it declared.

What arrives is taken as correct

Welt builds the payload and checks its own output against the wire contract before releasing it, so these two functions do no field validation of their own. A payload that departs from the contract is a bug on the sending side rather than an input to guard against, and it surfaces as an ordinary error from whatever touches it first — a KeyError, a TypeError, or binascii.Error from bytes that are not base64.

The one thing decode_messages refuses outright is a content block of a kind Welt never sends. A messages turn carries only text, image, document, and video blocks; a toolUse or toolResult block is not a malformed one of those but a forged conversation turn, and rebuilt into history it would let a caller that is not Welt put words the model treats as its own past tool calls and their results into the run. It raises ValueError. This is a trust-boundary check, not the field validation the contract otherwise saves you from.

Outbound

renderable_events(events, agent=..., files_from=...)

Reduces raw stream_async events — not JSON-serializable as-is — to the events Welt renders:

Strands emits On the wire In the Slack thread
Text deltas data The streamed reply
Tool invocations and results current_tool_use / tool_result "Using tool" indicators (tool output stays off the wire)
Image / document / video blocks the model produces, or a tool named in files_from returns file An uploaded file (size limits)
Pending interrupts interrupt Buttons and/or a text field

A run that stops for human input ends its stream with one interrupt event per pending interrupt; agents that do not use interrupts see no change.

A tool hands files to the model for either of two reasons — to have it read them, or to give them to the human — and only the agent knows which is which, so name the tools whose files belong in the thread:

async for event in renderable_events(
    stream, agent=agent, files_from={"generate_image"}
):

A tool left out keeps its files to the model: strands-tools' file_read reading a PDF does not drop it into the thread as a side effect. A tool named there needs no code of its own — strands-tools' generate_image returns the image as a tool-result block, and naming it is all it takes; a tool of your own returns image, document, or video blocks the same way. The agent is what makes the names resolvable: its messages hold the tool behind each result, the only place that survives a resume, where the stream carries the result alone.

Uploaded names come from the block — a document's own name plus its format, the block's kind for the rest (image.png). That name is the model's handle on the document as much as a filename, and Converse rejects a request whose messages carry two documents under one name, so a tool that returns documents has to keep their names apart across the run: strands-tools' file_read appends a short uuid to each.

Each event carries only what Welt reads. A current_tool_use is cut down to the name and id behind the indicator, so the tool's arguments — which Strands re-sends in full on every input delta — stay off the wire, and an event with nothing to render (a text chunk the model left empty, a file with no bytes) is not sent at all.

interrupt_reason(message, options=..., approve=..., reject=..., input=...)

Builds the structured reason Welt renders as a message with the specified widgets — the approve and reject buttons Welt words and values itself (approve, reject), choice buttons of your own (options), a free-text field (input), or any combination. approve and reject answer with True and False, so a question whose decision is approval asks for them by name instead of inventing values; {} takes Welt's wording, and a label or style overrides it. An option's value is any JSON value, and the pressed button answers with it as it was declared. With no widget at all the message renders as itself and Welt's default buttons answer it. The specs are the wire's own shapes, typed as DecisionSpec, OptionSpec, and InputSpec, and omitted fields keep Welt's defaults:

answer = tool_context.interrupt(
    "deploy-approval",
    reason=interrupt_reason(
        "Deploy to prod?",
        approve={"label": "Deploy"},
        reject={"label": "Cancel"},
        input={"label": "Or type your answer"},
    ),
)

Building the reason through this helper is what makes a typo an error. ToolContext.interrupt takes its reason as Any, so a dict literal handed to it directly is checked by nothing, and Welt's reaction to a reason it cannot match is its default buttons — no error, no log, just widgets you did not ask for. The typed parameters catch a misspelled key before the run; the checks inside catch it in runs where no type checker was involved. What they check is the shape, not the size: how many buttons one Slack block holds, and how long a button value may be, are Welt's to enforce.

Working with interrupts

Welt's Interrupts doc covers the Slack side: how each reason renders, who can answer, multiple questions, and expiry. On the Strands side:

  • Prefix your interrupt names (myapp-deploy-approval). Hook-raised interrupts must be unique across the whole event, tool-raised ones within their tool — a prefix keeps both as the agent grows.
  • Gate your own tool with interrupt(), and everything else with steering. A tool you wrote can ask for itself; a tool you did not — from strands-tools, or an MCP server — is gated from outside by a SteeringHandler returning Interrupt, which also puts the decision for every tool in one place. A handler cannot declare buttons, so its questions get Welt's default buttons and it reads the boolean they answer with. The example agent gates generate_image this way.
  • Strands' ready-made HumanInTheLoop intervention works over Welt as-is. Its string reasons render with Welt's default buttons, and its default evaluator reads the true they answer with as approval. Do not pass ask: both of its inline modes block the agent waiting for input that Slack can never deliver — the default interrupt/resume mode is the one Welt drives.
  • Route stdio consent prompts through interrupts instead. For strands-tools packages that gate themselves behind a stdio prompt, set BYPASS_TOOL_CONSENT=true and let HumanInTheLoop do the gating over Slack. The strands-tools handoff_to_user tool is likewise stdio-bound; a small interrupt-raising tool of your own is the replacement.
  • Code before interrupt runs again on resume. Strands re-executes the interrupted tool from its start, so whatever precedes an interrupt and must not run twice — side effects, or work that must match what the human approved — has to be skipped on the second pass. Memoizing on tool_context.tool_use["toolUseId"], the same id on both passes, is enough: the cache lives in the same process as the interrupt state it pairs with. The example agent's sample_draft_report shows the pattern.

License

MIT

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

welt_io_strands-0.8.1.tar.gz (31.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

welt_io_strands-0.8.1-py3-none-any.whl (18.6 kB view details)

Uploaded Python 3

File details

Details for the file welt_io_strands-0.8.1.tar.gz.

File metadata

  • Download URL: welt_io_strands-0.8.1.tar.gz
  • Upload date:
  • Size: 31.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.12.5 {"installer":{"name":"uv","version":"0.12.5","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for welt_io_strands-0.8.1.tar.gz
Algorithm Hash digest
SHA256 a18c33c6bbeddabdd21a30f466737e8ea85261ca1e81a7c9b2fdbe5d9fc372dc
MD5 18311bbfe02653aee2112e64ef1b823c
BLAKE2b-256 3578a8d70f3f548a8fc876f93ba8dea2f3c71bb245c2fc502dc70424a027edf3

See more details on using hashes here.

File details

Details for the file welt_io_strands-0.8.1-py3-none-any.whl.

File metadata

  • Download URL: welt_io_strands-0.8.1-py3-none-any.whl
  • Upload date:
  • Size: 18.6 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.12.5 {"installer":{"name":"uv","version":"0.12.5","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for welt_io_strands-0.8.1-py3-none-any.whl
Algorithm Hash digest
SHA256 6b5596ba2e3b4bdbf0e3dd87b937f2d9db889299036be90b085658f39a9a447a
MD5 cd4c49ab2fa2a6cbba4b7028db03064b
BLAKE2b-256 9686d04b8b2fec66ece141d1325b827cd23683c17740bc0788a7479957eabcbc

See more details on using hashes here.

Release history Release notifications | RSS feed

0.10.1

2 files

0.10.0

2 files

0.9.0

2 files

This release

0.8.1 This release

2 files

0.8.0

2 files

0.7.1

2 files

0.7.0

2 files

0.6.2

2 files

0.6.1

2 files

0.6.0

2 files

0.5.0

2 files

0.4.3

2 files

0.4.2

2 files

0.4.1

2 files

0.4.0

2 files

0.3.3

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page