|
| 1 | +# External Storage |
| 2 | + |
| 3 | +Run a Deep Agent whose `read_file` tool returns **6 MiB** of text, exceeding |
| 4 | +Temporal's default 2 MiB payload limit. The SDK's native `ExternalStorage` |
| 5 | +offloads the tool output and the next model activity's input to S3, recording |
| 6 | +small references in workflow history and retrieving the full bytes on decode. |
| 7 | +The workflow returns only a byte count, SHA-256, and a short final answer. |
| 8 | + |
| 9 | +Native External Storage is in **Public Preview**. This sample uses the SDK's |
| 10 | +`S3StorageDriver` with the local mock S3 service from the |
| 11 | +[general External Storage sample](../../external_storage). Neither AWS credentials, |
| 12 | +Docker, nor an LLM API key is needed. The model replies are scripted; the real |
| 13 | +Deep Agents loop, tool activity, model activities, and S3 transfers still run. |
| 14 | + |
| 15 | +## Running the sample |
| 16 | + |
| 17 | +Use Python **3.11 or later**. Run these commands from the repository root: |
| 18 | + |
| 19 | +```bash |
| 20 | +uv sync --python 3.13 --group deepagents --group external-storage |
| 21 | +``` |
| 22 | + |
| 23 | +Start Temporal with its default payload limits in one terminal: |
| 24 | + |
| 25 | +```bash |
| 26 | +temporal server start-dev |
| 27 | +``` |
| 28 | + |
| 29 | +In a second terminal, start the existing mock S3 service. It listens on port |
| 30 | +5000 and creates the `temporal-payloads` bucket: |
| 31 | + |
| 32 | +```bash |
| 33 | +uv run external_storage/s3.py |
| 34 | +``` |
| 35 | + |
| 36 | +In a third terminal, run the demo. It starts a worker, executes one workflow, |
| 37 | +checks the result's integrity, and shuts down the worker: |
| 38 | + |
| 39 | +```bash |
| 40 | +uv run deepagents_plugin/external_storage/main.py |
| 41 | +``` |
| 42 | + |
| 43 | +The output includes: |
| 44 | + |
| 45 | +```text |
| 46 | +Tool result: 6,291,456 bytes (6 MiB), integrity verified |
| 47 | +Agent: Received the complete document. |
| 48 | +``` |
| 49 | + |
| 50 | +The script also prints the workflow ID and complete SHA-256. Inspect the history |
| 51 | +in the Temporal UI or with: |
| 52 | + |
| 53 | +```bash |
| 54 | +temporal workflow show --workflow-id <printed-workflow-id> |
| 55 | +``` |
| 56 | + |
| 57 | +The completed `deepagents.invoke_tool` activity and the following |
| 58 | +`deepagents.invoke_model` input contain native storage references. The first |
| 59 | +model input and final workflow result remain small and inline. |
| 60 | + |
| 61 | +To view externally stored payloads in the UI, reuse the |
| 62 | +[storage-aware codec server](../../external_storage#5-optional-run-the-codec-server): |
| 63 | +run `uv run external_storage/codec_server.py` and set the UI's Remote Codec |
| 64 | +Endpoint to `http://localhost:8081`. Its gzip decoder passes uncompressed payloads |
| 65 | +through, so it can also read this sample's references. |
| 66 | + |
| 67 | +## How the configuration works |
| 68 | + |
| 69 | +`client.py` creates `ExternalStorage(drivers=[driver], |
| 70 | +payload_size_threshold=256 * 1024)`. It uses the SDK's `SimplePlugin` converter |
| 71 | +hook to apply this configuration **after** `DeepAgentsPlugin` installs its |
| 72 | +LangChain-aware payload converter: |
| 73 | + |
| 74 | +```python |
| 75 | +def configure(converter: DataConverter | None) -> DataConverter: |
| 76 | + return replace(converter or DataConverter.default, external_storage=storage) |
| 77 | + |
| 78 | +client = await Client.connect( |
| 79 | + "localhost:7233", |
| 80 | + plugins=[ |
| 81 | + create_plugin(), |
| 82 | + SimplePlugin("ExternalStorage", data_converter=configure), |
| 83 | + ], |
| 84 | +) |
| 85 | +``` |
| 86 | + |
| 87 | +This order also supports the older plugin version in this repository's lockfile, |
| 88 | +which otherwise replaces the client's converter. The worker inherits the final |
| 89 | +converter. The S3 client stays open while the worker runs and the starter decodes |
| 90 | +results. No application code uploads or downloads payloads manually. |
| 91 | + |
| 92 | +This demo deliberately uses **no compression codec**: repeated `x` characters |
| 93 | +would compress well below the storage threshold. It overrides the built-in |
| 94 | +`read_file` tool with an activity-backed mock bulk reader. Deep Agents excludes |
| 95 | +`read_file` from tool-result eviction, so the complete result crosses both |
| 96 | +activity boundaries. Applications using real models should page documents or |
| 97 | +retrieve excerpts to manage context separately. Storage reduces transport and |
| 98 | +history bytes; it does not reduce model tokens or decoded workflow memory. |
| 99 | + |
| 100 | +With storage removed, this tool result fails with `PayloadsTooLarge` at the |
| 101 | +default limits. Raising the blob limit alone still leaves the gRPC message |
| 102 | +limit described in [Hanyu Liu's post](https://hanyuliu.me/posts/temporal-deep-agent-payload-limits/). |
| 103 | +Native storage avoids transmitting the large bytes through either limit. |
| 104 | + |
| 105 | +For real S3, use an existing bucket and normal AWS credentials instead of this |
| 106 | +sample's local endpoint and mock credentials. Configure compatible storage |
| 107 | +drivers on every client, worker, and replayer that reads the history. Retain |
| 108 | +objects for as long as execution, reset, or replay may need them; Temporal does |
| 109 | +not delete stored payloads when an activity finishes. |
| 110 | + |
| 111 | +## Tests |
| 112 | + |
| 113 | +The integration test starts an isolated mock S3 service and verifies the full |
| 114 | +result, both native references, activity routing, and replay with a new S3 |
| 115 | +client after worker shutdown. |
| 116 | +No manually running S3 service or model credentials are required: |
| 117 | + |
| 118 | +```bash |
| 119 | +uv sync --python 3.13 --group deepagents --group external-storage --group dev |
| 120 | +uv run pytest tests/deepagents_plugin/external_storage_test.py |
| 121 | +``` |
0 commit comments