Skip to content
Merged
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
18 changes: 10 additions & 8 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,14 +77,16 @@ defines no placeholder API or compatibility path for it.

An embedding application may configure `AgentConfig.InactivityTimeoutSeconds`
and inject `InactivityExpirationHandler`. The Agent owns the inactivity
decision because only its durable state distinguishes a user wait from active
model/tool work and `durable_wait`. `AwaitUser`, `AwaitToolApproval`, and
`AwaitManualToolRecovery` persist `InactivityDeadline` and race their existing
Channels with one Dex Timer. Accepted input wins when both are ready. All other
Steps expose a null deadline. Expiration first commits `expiring` and one
`inactivity_expired` event, then retries the stable Flow ID, Run ID, deadline,
and runtime-metadata callback for up to seven days. Only callback success closes
the Agent Flow; cross-resource cleanup remains the embedding application's job.
decision through one `InactivityTimeout` branch started beside the Agent loop.
Accepted user RPCs atomically advance `InactivityDeadline` and publish
`ResetInactivityTimer` only when the extension exceeds one minute. The Agent
RPC handler samples the current time after validating the accepted operation.
Model and ordinary tool work do not pause the deadline. `DurableWait` extends
it to the wait target plus the configured timeout. Expiration retries the stable
Flow ID, Run ID, deadline, and runtime-metadata callback for up to seven days.
Callback success commits
`expiring`, emits `inactivity_expired`, and force-completes the Agent; cross-
resource cleanup remains the embedding application's job.

`CurrentMessages` and `ArchivedMessages` are the typed application history;
they are not Dex execution history. `AgentState` owns the retained sequence
Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ check-flow-definition: install-dexcli
echo "Flow definition must contain the canonical ExecuteTool Step" >&2; \
exit 1; \
fi; \
for channel in answeredUserInputsChannel queuedUserMessagesChannel steeredUserMessagesChannel toolApprovalsChannel toolRecoveryDecisionsChannel parallelToolResultsChannel planExecutionsChannel; do \
for channel in resetInactivityTimerChannel answeredUserInputsChannel queuedUserMessagesChannel steeredUserMessagesChannel toolApprovalsChannel toolRecoveryDecisionsChannel parallelToolResultsChannel planExecutionsChannel; do \
if ! grep -Fq "\"id\": \"resource:channel:$${channel}\"" "$${flow_definition}" || \
! grep -Fq "\"resourceId\": \"resource:channel:$${channel}\"" "$${flow_definition}"; then \
echo "Flow definition must render Channel $${channel} and its WaitFor edge" >&2; \
Expand Down
12 changes: 6 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,14 +70,14 @@ typed `StartRequest`. `RuntimeMetadata` is an optional JSON object of at most
passed only to tool implementations, never to models, browser Snapshots, or
Streams. Do not put secrets in it.

Set `AgentConfig.InactivityTimeoutSeconds` to arm inactivity expiration while
the Agent waits for a message, question answer, approval, or recovery decision;
zero disables it. Embedders enabling the timeout must pass
Set `AgentConfig.InactivityTimeoutSeconds` to arm inactivity expiration across
the Agent lifecycle; zero disables it and positive values start at two minutes.
Embedders enabling the timeout must pass
`agent.WithInactivityExpirationHandler(...)` to `agent.NewFlow`. The handler
receives a stable Flow ID, Run ID, deadline, and runtime metadata for an
idempotent application cleanup request. Snapshot exposes `inactivityDeadline`
only while the Timer is armed; model work, tool work, and `durable_wait` pause
the timeout.
idempotent application cleanup request. Accepted user mutations coalesce Timer
resets within one minute. A `durable_wait` extends the deadline through its
target; model and ordinary tool work do not pause it.

External tool retries belong to Dex. `ToolDefinition` controls attempt timeout,
maximum attempts, and total duration. A registry performs exactly one call for
Expand Down
31 changes: 16 additions & 15 deletions docs/adr/0016-agent-owned-inactivity-expiration.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,32 +6,33 @@ Accepted on 2026-09-23.

## Context

Session infrastructure cannot infer inactivity from API traffic. Snapshot and
Stream reads are observation, model and tool calls are active work, and a
`durable_wait` intentionally suspends longer than an inactivity window. Copying
Session infrastructure cannot infer user inactivity from API traffic. Snapshot
and Stream reads are observation, while a `durable_wait` may intentionally
suspend longer than an inactivity window. Copying
deadlines into multiple lifecycle Flows also requires publishing every activity
to every resource owner and creates competing shutdown decisions.

## Decision

`AIAgentFlow` owns the inactivity decision. A positive
`inactivity_timeout_seconds` arms a Dex Timer only in `AwaitUser`,
`AwaitToolApproval`, and `AwaitManualToolRecovery`. The same atomic wait writes
`InactivityDeadline`; every active-work or durable-wait path leaves it absent.
Accepted Channel input takes precedence if it becomes ready with the Timer.

Expiration commits the `expiring` status and one `inactivity_expired` activity
event before invoking the constructor-injected `InactivityExpirationHandler`.
`inactivity_timeout_seconds` starts one parallel `InactivityTimeout` Step and
persists `InactivityDeadline`. Accepted user RPCs advance the Attribute and
publish `ResetInactivityTimer` only when the extension exceeds one minute.
The accepted RPC handler samples the current time after business validation.
Model and ordinary tool work do not pause the deadline. `DurableWait` advances
it to the wait target plus the configured timeout.

Expiration invokes the constructor-injected `InactivityExpirationHandler`.
The handler receives Flow ID, Run ID, deadline, and trusted runtime metadata.
Those values form a stable idempotency identity across retries and Worker
replacement. Dex retries the callback for a bounded seven-day window, and the
Agent completes only after the callback succeeds.
Agent commits `expiring`, emits `inactivity_expired`, and force-completes only
after the callback succeeds.

## Consequences

Embedders configure the timeout and own cross-resource cleanup behind the
handler. They do not report each activity back to lifecycle infrastructure.
Snapshots expose the exact deadline only while inactivity is armed, allowing a
UI to distinguish an active countdown from paused model, tool, and durable-wait
work. A failed callback leaves the Agent durably `expiring` until retry succeeds
or the explicit retry policy is exhausted.
Snapshots expose the exact deadline for the configured Agent lifecycle. A
failed callback retains the expired deadline until retry succeeds or the
explicit retry policy is exhausted.
29 changes: 17 additions & 12 deletions docs/flow-model.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,8 +105,12 @@ DurableWait
-> next tool or CompactContext (timer fired)
-> CompactContext (steered)

AwaitUser / AwaitToolApproval / AwaitManualToolRecovery
-> BeginInactivityExpiration -> ExpireInactivity (inactivity Timer fired)
Init
-> AwaitUser + InactivityTimeout (timeout configured)

InactivityTimeout
-> InactivityTimeout (reset deadline)
-> ForceComplete (handler succeeded)
```

### Tool-call routing
Expand Down Expand Up @@ -168,8 +172,7 @@ history, and makes the model replan.
| `PrepareManualToolRecovery` | none | Capture a serial returned-unknown or exhausted call and its redacted error type |
| `AwaitManualToolRecovery` | exact recovery decision or steering | Persist recovery state; retry selected calls, continue unknowns, stop the sequence, or replan |
| `DurableWait` | Timer or steering | Persist waiting status; record completion or interruption and continue |
| `BeginInactivityExpiration` | none | Clear the deadline, persist `expiring`, emit one expiration event, and schedule the callback |
| `ExpireInactivity` | none | Invoke the application handler with stable identity under bounded retry; complete only after success |
| `InactivityTimeout` | reset timestamp or deadline Timer | Coalesce reset deadlines, invoke the application handler under bounded retry, and force-complete only after success |

### Tool StepOptions resolution

Expand Down Expand Up @@ -209,13 +212,14 @@ Approval and timer waits never change the round. The
next value must remain within JavaScript's safe integer range or the WaitFor
fails explicitly.

When `inactivity_timeout_seconds` is positive, the three user-controlled waits
append a Dex Timer and persist its exact `InactivityDeadline`. Ready answers,
messages, plan execution, steering, approvals, and recovery decisions are read
before the Timer outcome, so accepted input wins a simultaneous wake. Leaving a
user wait deletes the deadline. Model calls, serial or parallel tools, and
`DurableWait` never carry an inactivity Timer. After a durable wait completes or
is steered, the next user wait receives a complete new timeout window.
When `inactivity_timeout_seconds` is positive, `Init` starts one parallel
`InactivityTimeout` Step and persists its exact `InactivityDeadline`. Accepted
messages, answers, plan execution, steering, queue deletion, approvals, and
recovery decisions advance the deadline transactionally. Extensions of one
minute or less do not publish another reset. The accepted RPC handler samples
the current time after business validation. Snapshot and Stream reads do not
reset it. Model calls and serial or parallel tools continue under the same
deadline. `DurableWait` advances it to the wait target plus the timeout.

## Durable resources

Expand All @@ -233,7 +237,8 @@ is steered, the next user wait receives a complete new timeout window.
| `PendingToolRecovery` | Attribute | Exact recovery revision and redacted failed-call presentation |
| `PendingTimer` | Attribute | Current durable wait presentation |
| `PendingUserInput` | Attribute | Current structured question batch |
| `InactivityDeadline` | Attribute | Exact deadline only while one user-controlled wait is armed |
| `InactivityDeadline` | Attribute | Current global deadline while inactivity expiration is configured |
| `ResetInactivityTimer` | Channel | Coalesced UTC deadline updates for the timeout branch |
| `AnsweredUserInputs` | Channel | Validated answers awaiting immediate consumption |
| `QueuedUserMessages` | Channel | FIFO `PendingUserMessage` payloads with stable application IDs |
| `SteeredUserMessages` | Channel | Safe-boundary steering payloads preserving the same application IDs |
Expand Down
Loading