Skip to content

Commit 65abf78

Browse files
stainless-app[bot]danielmillerpclaudemax-parke-scale
authored
chore: release main (#456)
Co-authored-by: Daniel Miller <daniel.miller@scale.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: Max Parke <max.parke@scale.com> Co-authored-by: stainless-app[bot] <142633134+stainless-app[bot]@users.noreply.github.com>
1 parent 936bac6 commit 65abf78

13 files changed

Lines changed: 293 additions & 61 deletions

File tree

.release-please-manifest.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
{
2-
".": "0.16.2",
3-
"adk": "0.16.2"
2+
".": "0.17.0",
3+
"adk": "0.17.0"
44
}

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,14 @@
1212

1313
* **tracing:** emit OTel metrics for async span queue depth, batch drain, and SGP export success/failure (HTTP status labels). Disable SDK-side recording with ``AGENTEX_TRACING_METRICS=0``.
1414

15+
## 0.17.0 (2026-07-01)
16+
17+
Full Changelog: [agentex-client-v0.16.2...agentex-client-v0.17.0](https://github.com/scaleapi/scale-agentex-python/compare/agentex-client-v0.16.2...agentex-client-v0.17.0)
18+
19+
### Features
20+
21+
* **temporal:** opt-in continue-as-new for long-lived agent workflows ([#447](https://github.com/scaleapi/scale-agentex-python/issues/447)) ([98cf744](https://github.com/scaleapi/scale-agentex-python/commit/98cf7444002b5f9862f3a922665f016ae6c89af0))
22+
1523
## 0.16.2 (2026-06-29)
1624

1725
Full Changelog: [agentex-client-v0.16.1...agentex-client-v0.16.2](https://github.com/scaleapi/scale-agentex-python/compare/agentex-client-v0.16.1...agentex-client-v0.16.2)

adk/CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
# Changelog
22

3+
## 0.17.0 (2026-07-01)
4+
5+
Full Changelog: [agentex-sdk-v0.16.2...agentex-sdk-v0.17.0](https://github.com/scaleapi/scale-agentex-python/compare/agentex-sdk-v0.16.2...agentex-sdk-v0.17.0)
6+
7+
### Chores
8+
9+
* **agentex-sdk:** Synchronize agentex versions
10+
311
## 0.16.2 (2026-06-29)
412

513
Full Changelog: [agentex-sdk-v0.15.0...agentex-sdk-v0.16.2](https://github.com/scaleapi/scale-agentex-python/compare/agentex-sdk-v0.15.0...agentex-sdk-v0.16.2)

adk/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
# (agentex/{__init__.py, _*.py, types/, resources/}) ships from the slim
55
# sibling package `agentex-client` which is pinned as a runtime dep.
66
name = "agentex-sdk"
7-
version = "0.16.2"
7+
version = "0.17.0"
88
description = "Agent Development Kit (ADK) overlay for the Agentex API — FastACP server, Temporal workflows, LLM provider integrations, observability"
99
license = "Apache-2.0"
1010
authors = [

examples/tutorials/10_async/10_temporal/000_hello_acp/project/workflow.py

Lines changed: 23 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -53,19 +53,27 @@ async def on_task_event_send(self, params: SendEventParams) -> None:
5353
async def on_task_create(self, params: CreateTaskParams) -> None:
5454
logger.info(f"Received task create params: {params}")
5555

56-
# 1. Acknowledge that the task has been created.
57-
await adk.messages.create(
58-
task_id=params.task.id,
59-
content=TextContent(
60-
author="agent",
61-
content=f"Hello! I've received your task. Normally you can do some state initialization here, or just pass and do nothing until you get your first event. For now I'm just acknowledging that I've received a task with the following params:\n\n{json.dumps(params.params, indent=2)}.\n\nYou should only see this message once, when the task is created. All subsequent events will be handled by the `on_task_event_send` handler.",
62-
),
63-
)
56+
# 1. Acknowledge that the task has been created. Gate this one-time prologue
57+
# on is_continued_run(): run_until_complete below recycles the workflow via
58+
# continue-as-new, which re-enters on_task_create from the top — without this
59+
# guard the "you should only see this once" welcome would re-fire on every
60+
# recycle. Original run -> emit; continued (recycled) run -> skip.
61+
if not self.is_continued_run():
62+
await adk.messages.create(
63+
task_id=params.task.id,
64+
content=TextContent(
65+
author="agent",
66+
content=f"Hello! I've received your task. Normally you can do some state initialization here, or just pass and do nothing until you get your first event. For now I'm just acknowledging that I've received a task with the following params:\n\n{json.dumps(params.params, indent=2)}.\n\nYou should only see this message once, when the task is created. All subsequent events will be handled by the `on_task_event_send` handler.",
67+
),
68+
)
6469

65-
# 2. Wait for the task to be completed indefinitely. If we don't do this the workflow will close as soon as this function returns. Temporal can run hundreds of millions of workflows in parallel, so you don't need to worry about too many workflows running at once.
66-
67-
# Thus, if you want this agent to field events indefinitely (or for a long time) you need to wait for a condition to be met.
68-
await workflow.wait_condition(
69-
lambda: self._complete_task,
70-
timeout=None, # Set a timeout if you want to prevent the task from running indefinitely. Generally this is not needed. Temporal can run hundreds of millions of workflows in parallel and more. Only do this if you have a specific reason to do so.
71-
)
70+
# 2. Keep the workflow open to field events. We use run_until_complete
71+
# instead of a bare wait_condition: it still waits indefinitely, but also
72+
# recycles the Temporal event history via continue-as-new before it hits the
73+
# ~50k-event / 50MB limit, so this chat can stay open forever. Adopting
74+
# run_until_complete IS the opt-in — agents that keep the old wait_condition
75+
# never recycle. This agent keeps no cross-turn state, so nothing needs
76+
# restoring across a recycle and `params` is the only carry-forward. (Agents
77+
# that DO keep state restore it at the top of @workflow.run on a recycled
78+
# run — framework-specific, landing per-integration in follow-up PRs.)
79+
await self.run_until_complete(params, is_complete=lambda: self._complete_task)

examples/tutorials/10_async/10_temporal/010_agent_chat/tests/test_agent.py

Lines changed: 19 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -240,42 +240,28 @@ async def stream_messages() -> None:
240240
task_id=task.id,
241241
timeout=90, # Increased timeout for CI environments
242242
):
243+
# A turn emits several messages (user echo, reasoning, agent text),
244+
# each ending in "full" or "done"; consume until the text reply lands.
243245
msg_type = event.get("type")
244246
if msg_type == "full":
245-
task_message_update = StreamTaskMessageFull.model_validate(event)
246-
if task_message_update.parent_task_message and task_message_update.parent_task_message.id:
247-
finished_message = await client.messages.retrieve(task_message_update.parent_task_message.id)
248-
if (
249-
finished_message.content
250-
and finished_message.content.type == "text"
251-
and finished_message.content.author == "user"
252-
):
253-
user_message_found = True
254-
elif (
255-
finished_message.content
256-
and finished_message.content.type == "text"
257-
and finished_message.content.author == "agent"
258-
):
259-
agent_response_found = True
260-
elif finished_message.content and finished_message.content.type == "reasoning":
261-
reasoning_found = True
262-
263-
# Exit early if we have what we need
264-
if user_message_found and agent_response_found:
265-
break
266-
247+
parent_task_message = StreamTaskMessageFull.model_validate(event).parent_task_message
267248
elif msg_type == "done":
268-
task_message_update_done = StreamTaskMessageDone.model_validate(event)
269-
if task_message_update_done.parent_task_message and task_message_update_done.parent_task_message.id:
270-
finished_message = await client.messages.retrieve(task_message_update_done.parent_task_message.id)
271-
if finished_message.content and finished_message.content.type == "reasoning":
272-
reasoning_found = True
273-
elif (
274-
finished_message.content
275-
and finished_message.content.type == "text"
276-
and finished_message.content.author == "agent"
277-
):
278-
agent_response_found = True
249+
parent_task_message = StreamTaskMessageDone.model_validate(event).parent_task_message
250+
else:
251+
continue
252+
253+
if parent_task_message and parent_task_message.id:
254+
finished_message = await client.messages.retrieve(parent_task_message.id)
255+
content = finished_message.content
256+
if content and content.type == "text" and content.author == "user":
257+
user_message_found = True
258+
elif content and content.type == "text" and content.author == "agent":
259+
agent_response_found = True
260+
elif content and content.type == "reasoning":
261+
reasoning_found = True
262+
263+
# Stop once both the user echo and the agent's text reply are seen.
264+
if user_message_found and agent_response_found:
279265
break
280266

281267
stream_task = asyncio.create_task(stream_messages())

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
# overlay (formerly `src/agentex/lib/*`) now lives in `adk/` and ships
44
# as the sibling `agentex-sdk` package — see `adk/pyproject.toml`.
55
name = "agentex-client"
6-
version = "0.16.2"
6+
version = "0.17.0"
77
description = "The official Python REST client for the Agentex API"
88
dynamic = ["readme"]
99
license = "Apache-2.0"

src/agentex/_version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
22

33
__title__ = "agentex"
4-
__version__ = "0.16.2" # x-release-please-version
4+
__version__ = "0.17.0" # x-release-please-version

src/agentex/lib/core/clients/temporal/temporal_client.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,7 @@ async def start_workflow(
153153
duplicate_policy: DuplicateWorkflowPolicy = DuplicateWorkflowPolicy.ALLOW_DUPLICATE,
154154
retry_policy: RetryPolicy = DEFAULT_RETRY_POLICY,
155155
task_timeout: timedelta = timedelta(seconds=10),
156-
execution_timeout: timedelta = timedelta(seconds=86400),
156+
execution_timeout: timedelta | None = None,
157157
**kwargs: Any,
158158
) -> str:
159159
temporal_retry_policy = TemporalRetryPolicy(**retry_policy.model_dump(exclude_unset=True))

src/agentex/lib/core/temporal/services/temporal_task_service.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,15 @@ async def submit_task(self, agent: Agent, task: Task, params: dict[str, Any] | N
3333
3434
returns the workflow ID of the temporal workflow
3535
"""
36+
# None / 0 / negative => no execution timeout (workflow can stay open
37+
# indefinitely, which long-lived chat/session agents rely on). A positive
38+
# value bounds the whole continue-as-new chain's wall-clock lifetime.
39+
timeout_seconds = self._env_vars.WORKFLOW_EXECUTION_TIMEOUT_SECONDS
40+
execution_timeout = (
41+
timedelta(seconds=timeout_seconds)
42+
if timeout_seconds and timeout_seconds > 0
43+
else None
44+
)
3645
return await self._temporal_client.start_workflow(
3746
workflow=self._env_vars.WORKFLOW_NAME,
3847
arg=CreateTaskParams(
@@ -42,7 +51,7 @@ async def submit_task(self, agent: Agent, task: Task, params: dict[str, Any] | N
4251
),
4352
id=task.id,
4453
task_queue=self._env_vars.WORKFLOW_TASK_QUEUE,
45-
execution_timeout=timedelta(seconds=self._env_vars.WORKFLOW_EXECUTION_TIMEOUT_SECONDS),
54+
execution_timeout=execution_timeout,
4655
)
4756

4857
async def get_state(self, task_id: str) -> WorkflowState:

0 commit comments

Comments
 (0)