You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit 9d71bb7
Browse filesBrowse the repository at this point in the historyBrowse files
feat(temporal): opt-in continue-as-new for long-lived agent workflows
Long-lived chat/session agents run as a single Temporal workflow that stays
open indefinitely, so their event history grows until it hits Temporal's
~50k-event / 50MB limit and the workflow stalls. This adds an opt-in
continue-as-new path that recycles the history so a session can stay open
forever, plus the discipline of keeping messages/state outside workflow state
so they survive the recycle.
SDK (BaseWorkflow):
- should_continue_as_new(): recycle decision (Temporal's is_continue_as_new_
suggested() or a configurable WORKFLOW_MAX_HISTORY_LENGTH threshold).
- drain_and_continue_as_new(): waits all_handlers_finished (so an in-flight
turn is never lost/duplicated at the boundary) then continue_as_new.
- run_until_complete(): drop-in replacement for the usual
wait_condition(timeout=None) tail; gated once behind workflow.patched() so
in-flight pre-patch workflows keep the old behaviour (no non-determinism on
replay). Identical behaviour unless WORKFLOW_CONTINUE_AS_NEW_ENABLED is set.
- conversation_from_messages(): rebuild the conversation from the adk.messages
ledger after a recycle (messages live in adk.messages, not workflow state).
Config (default off, so existing agents are unaffected):
- WORKFLOW_CONTINUE_AS_NEW_ENABLED (bool)
- WORKFLOW_MAX_HISTORY_LENGTH (int|None)
Examples: all 13 long-lived Temporal tutorial agents adopt run_until_complete.
Message-based chat agents rebuild conversation from adk.messages; harness
agents with an opaque session handle (claude-code, codex, claude-sdk) or rich
history (pydantic-ai via ModelMessagesTypeAdapter, langgraph) persist their
non-message state to adk.state and re-hydrate on recycle. Every adk.state /
adk.messages round-trip is guarded by the enabled flag, so the default path is
byte-for-byte unchanged.
Note: continue-as-new bounds history SIZE; it does NOT extend the chain-wide
WORKFLOW_EXECUTION_TIMEOUT_SECONDS (raise that to keep workflows long-lived).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
# 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
66
67
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
-
awaitworkflow.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
-
)
68
+
# run_until_complete behaves exactly like the old indefinite wait_condition
69
+
# by default; when WORKFLOW_CONTINUE_AS_NEW_ENABLED is set it also recycles
70
+
# event history via continue-as-new before Temporal's history limit. This
71
+
# agent keeps no cross-turn conversation state, so the only thing carried
# 1. Initialize the state. You can either do this here or in the __init__ method.
262
-
# This function is triggered whenever a client creates a task for this agent.
263
-
# It is not re-triggered when a new event is sent to the task.
264
-
self._state=StateModel(
265
-
input_list=[],
266
-
turn_number=0,
267
-
)
268
-
269
-
# 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.
270
-
271
-
# 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.
272
-
273
-
awaitworkflow.wait_condition(
274
-
lambda: self._complete_task,
275
-
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.
276
-
)
280
+
# 1. Initialize (or, on a continued run, re-hydrate) the state. This
281
+
# function runs both when a client first creates the task AND when the
282
+
# workflow recycles itself via continue-as-new — on the latter the
283
+
# in-memory state was reset, so we reload it from the adk.messages ledger.
284
+
awaitself._rehydrate_state(params.task.id)
285
+
286
+
# 2. Keep the workflow open to field events. Temporal can run hundreds of
287
+
# millions of workflows in parallel, so staying open is cheap — but a single
288
+
# ever-open run accumulates event history until it hits Temporal's ~50k-event
289
+
# / 50MB limit. run_until_complete keeps the workflow open exactly like the
290
+
# old indefinite wait, and (when WORKFLOW_CONTINUE_AS_NEW_ENABLED is set)
291
+
# recycles history via continue-as-new before that limit. The conversation
292
+
# is rebuilt from adk.messages on each run, so the carry-forward is just
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.",
346
-
),
347
-
)
348
-
349
-
awaitworkflow.wait_condition(
350
-
lambda: self._complete_task,
351
-
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.
352
-
)
348
+
ifnotself.is_continued_run():
349
+
awaitadk.messages.create(
350
+
task_id=params.task.id,
351
+
content=TextContent(
352
+
author="agent",
353
+
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.",
354
+
),
355
+
)
356
+
357
+
# Keep the workflow open to field events. run_until_complete behaves exactly
358
+
# like the old indefinite wait_condition, and (when
359
+
# WORKFLOW_CONTINUE_AS_NEW_ENABLED is set) recycles event history via
360
+
# continue-as-new before Temporal's ~50k-event / 50MB limit. Messages live in
361
+
# adk.messages and are rebuilt above, so the only state carried forward is
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.",
231
-
),
232
-
)
233
-
234
-
# Keep workflow running indefinitely to handle user messages and human approvals
235
-
# This survives system failures and can resume exactly where it left off
236
-
awaitworkflow.wait_condition(
237
-
lambda: self._complete_task,
238
-
timeout=None, # No timeout for long-running human-in-the-loop workflows
239
-
)
240
+
ifnotself.is_continued_run():
241
+
awaitadk.messages.create(
242
+
task_id=params.task.id,
243
+
content=TextContent(
244
+
author="agent",
245
+
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.",
246
+
),
247
+
)
248
+
249
+
# Keep the workflow open to handle user messages and human approvals.
250
+
# run_until_complete behaves exactly like the old indefinite wait_condition,
251
+
# and (when WORKFLOW_CONTINUE_AS_NEW_ENABLED is set) recycles event history
252
+
# via continue-as-new before Temporal's ~50k-event / 50MB limit, draining any
253
+
# in-flight approval turn first. Messages live in adk.messages and are rebuilt
254
+
# above, so the only state carried forward is params.
0 commit comments