Skip to content

fix(has_context): skip persisting a generation paused for user input - #10

Draft
TonsOfFun wants to merge 3 commits into
mainfrom
fix/skip-paused-generations
Draft

TonsOfFun wants to merge 3 commits into
mainfrom
fix/skip-paused-generations

Conversation

@TonsOfFun

Copy link
Copy Markdown
Contributor

Closes #9

Why

The framework is about to gain a way to pause a generation for user input, so a tool call can ask the user a question or wait for approval and then continue. A paused response ends on an unfinished turn: an assistant tool-call turn or a tool result. HasContext would record that turn as a generation and an assistant row. When the generation resumes, its prompt replays the stored conversation, so the after_prompt callback would store the user turn a second time.

This change makes HasContext handle both sides of a pause. Both checks are duck-typed, so framework releases that cannot pause behave exactly as before, and nothing here depends on an unreleased activeagent.

What changes

  • persist_generation_to_context returns early, with an info log, when the response answers awaiting_input? with true. The paused response writes no generation record, assistant row or tool rows.
  • A generation that pauses still persists its prompt, the user turn. This is a deliberate departure from the earlier proposal to skip both callbacks on pause: the user turn should survive a request that is declined or never answered.
  • persist_prompt_to_context returns early while resuming_generation? is true. The paused run already stored the user turn, and the resumed prompt replays it.
  • New private resuming_generation?. It is true when the agent defines a public resuming? (the framework flag) that returns truthy, or after self.resuming_generation = true. The private writer is for a host that replays a stored conversation itself and sets the flag from an action or a callback.
    • Both methods are private because every public method on an agent becomes one of its actions, and the action list feeds the framework's release digest.
    • No prompt option is used, because unknown prompt_options keys reach the provider request.
    • solid_agent does not define resuming?, so a framework Base#resuming? is never overridden.
  • persist_tool_messages_to_context dedupes by tool_call_id within one response's stack as well as against rows already on the context. A resumed stack repeats the restored tool results, so each is stored once even when the context's messages scope cannot see rows written earlier in the same pass.
  • README (HasContext section):
    • what a paused generation persists and what the resumed one persists;
    • that the resumed generation must load the paused generation's context, through a contextual: param naming the same record or load_<name>(context_id:) in the action. Otherwise each agent instance creates its own anonymous context.
  • CHANGELOG (Unreleased): "Added" for resuming_generation? and its writer; "Fixed" for the persistence change and the in-stack dedupe. The version is not bumped.

Tests

New file test/solid_agent/has_context_paused_generation_test.rb (13 tests, using fake response and context objects). It covers:

  • the resume flag and its private visibility;
  • prompt persistence for fresh and resumed generations;
  • a paused generation persisting its prompt but not its response;
  • paused versus non-paused responses;
  • in-stack tool dedupe;
  • a full pause-then-resume run that leaves exactly the user turn, both tool results and the final answer.

All runs on Ruby 3.4.8:

  • bundle exec ruby -Itest -Ilib test/solid_agent/has_context_paused_generation_test.rb gives 13 runs, 28 assertions, 0 failures, 0 errors, 0 skips. Against the origin/main library the same file gives 13 runs, 4 failures, 5 errors.
  • bundle exec rake test gives 251 runs, 595 assertions, 0 failures, 0 errors, 0 skips. Baseline on origin/main: 238 runs, 567 assertions, 0 failures, 0 errors.
  • bundle exec rake test:records gives 195 runs, 494 assertions, 0 failures, 0 errors, 0 skips (same as baseline).
  • RuboCop with rails-omakase on the changed Ruby files finds no offenses. The repo has no RuboCop config, so I used a temporary inherit_gem: { rubocop-rails-omakase: rubocop.yml } config.

Not run:

  • The suite under Ruby 4.0, where require "ostruct" in test_helper fails. That is unrelated to this change, and CI tests 3.2 to 3.4.
  • activeagent's cross-repo solid_agent integration suite, after the review fixes. It was unchanged when run against the first commit, and the fixes only change method visibility and docs.

🤖 Generated with Claude Code

TonsOfFun and others added 3 commits October 2, 2026 18:31
A framework that can pause a generation for user input returns a response
whose last message is an unfinished turn: an assistant tool-call turn or a
tool result. capture_and_persist_generation recorded that as a generation
and an assistant row. When the generation later resumes, its prompt
messages are the restored conversation, so the after_prompt callback
persist_prompt_to_context stored the restored turn as a new user message.

persist_generation_to_context now returns early for a response answering
awaiting_input? with true, and persist_prompt_to_context returns early while
resuming_generation? is true. The agent counts as resuming when it defines a
public resuming? (the framework flag) that returns true, or when the host
sets self.resuming_generation = true for a replay it drives itself. Both
checks are duck-typed, so framework releases without a pause behave as
before.

persist_tool_messages_to_context already skipped a tool_call_id present on
the context; it now also skips one seen earlier in the same stack, so a
resumed stack that repeats a call persists it once even when the context's
messages scope cannot see rows written in the same pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ActiveAgent treats every public method an agent class gains as one of its
actions, and the action list is part of the release manifest the framework
digests into an agent's version. Public resuming_generation? and
resuming_generation= would therefore change the digest of every agent that
includes HasContext on upgrade, and calling either on the class would build
a generation through method_missing.

Both now live in the private section. A host still sets the flag with
self.resuming_generation = true from an action or a callback, since a
private writer accepts a literal self receiver. The CHANGELOG lists the
predicate and writer under Added.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The README and CHANGELOG said a generation that pauses for user input
persists nothing. The prompt callback only checks for a resume, so the
paused run still stores its prompt as the user turn; only the response
(generation record, assistant row, tool rows) is skipped. A host that
believed the docs could add the user turn again on resume and store it
twice. Keeping the user turn at pause time is deliberate: it survives a
request that is declined or never answered.

The README also now says the resumed generation must load the paused
generation's context. With an anonymous context each agent instance
creates its own, so the user turn and the answer would land in two
contexts.

The prompt callback's comment now gives the reason that holds however the
framework rebuilds the resumed prompt: the paused run already stored the
user turn. A new test pins the paused run's prompt row, and the
pause-then-resume test is renamed for what it asserts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@superconductor-for-github

Copy link
Copy Markdown

⏳ Superconductor is working — View implementation


I'll get back to you soon!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Skip persisting a generation paused for user input and persist its resume once

1 participant