Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ to include examples, links to docs, or any other relevant information.

### Fixed

- `temporalio.contrib.deepagents.run_deep_agent` no longer duplicates the original
input messages when carrying state through continue-as-new.
- `StrandsPlugin` now disables Botocore retries for its default Bedrock model so
model request retries are handled exclusively by Temporal.
- `temporalio.contrib.openai_agents` now honors the `retry-after-ms` and
Expand Down
13 changes: 3 additions & 10 deletions temporalio/contrib/deepagents/workflow.py
Original file line number Diff line number Diff line change
Expand Up @@ -158,23 +158,16 @@ async def call_backend_op(


def _merge_snapshot(input: Any, snapshot: Mapping[str, Any]) -> Any:
"""Prepend a snapshot's carried messages onto the next turn's input."""
"""Restore a snapshot's carried messages for the next turn."""
raw_prior: Any = snapshot.get("messages") or []
prior = list(raw_prior)
if not prior:
return input
if isinstance(input, Mapping):
merged = dict(input)
raw_next: Any = input.get("messages") or []
merged["messages"] = [*prior, *list(raw_next)]
merged["messages"] = prior
return merged
return {"messages": [*prior, *_as_message_list(input)]}


def _as_message_list(input: Any) -> list[Any]:
if isinstance(input, (list, tuple)):
return list(input)
return [input]
return {"messages": prior}


async def run_deep_agent(
Expand Down
10 changes: 4 additions & 6 deletions tests/contrib/deepagents/test_continue_as_new.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ class FakeAgent:
async def ainvoke(self, input: Any) -> dict:
messages = list(input.get("messages", [])) if isinstance(input, dict) else []
messages = [*messages, "step"]
done = len(messages) >= 3
done = messages.count("step") >= 3
return {
"messages": messages,
"todos": [
Expand All @@ -51,7 +51,7 @@ class ContinueAsNewWorkflow:
@workflow.run
async def run(self, input: dict, state_snapshot: dict | None = None) -> dict:
# Threshold of 1 means: continue-as-new as soon as there is pending work,
# which the fake agent reports until the conversation reaches 3 messages.
# which the fake agent reports until it has appended 3 steps.
return await run_deep_agent(
FakeAgent(),
input,
Expand All @@ -77,9 +77,7 @@ async def test_can_threshold_and_cache(env: WorkflowEnvironment) -> None:
)
result = await handle.result()

# The only way the conversation reaches >= 3 messages is if the snapshot from
# the pre-continue-as-new run was carried into the continued run and merged.
assert len(result["messages"]) >= 3, result
assert result["messages"] == ["start", "step", "step", "step"], result
assert result["todos"][0]["status"] == "completed"


Expand Down Expand Up @@ -155,7 +153,7 @@ async def test_can_defaults_to_server_suggestion(

# Carry across the suggested continue-as-new: the conversation only reaches
# 3 messages if snapshots crossed run boundaries.
assert len(result["messages"]) >= 3, result
assert result["messages"] == ["start", "step", "step"], result
assert result["todos"][0]["status"] == "completed"
# The first run really did continue-as-new (not complete).
first = env.client.get_workflow_handle(
Expand Down