Skip to content

Add autonomous continuation mode - #222

Closed
sethkarten wants to merge 5 commits into
mainfrom
feature/autonomous-user-sim
Closed

Add autonomous continuation mode#222
sethkarten wants to merge 5 commits into
mainfrom
feature/autonomous-user-sim

Conversation

@sethkarten

@sethkarten sethkarten commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

Summary

  • add opt-in autonomous mode via --autonomous and /autonomous on|off|status
  • continue host-side until terminal evidence exists instead of trusting assistant prose like “done” or “blocked”
  • treat a git worktree delta relative to the autonomous baseline as terminal evidence when no gates are configured
  • add optional autonomous quality gates with --autonomous-gate, --autonomous-gate-retries, and --autonomous-gate-timeout-ms
  • feed failed gate output back into the session so the agent can continue fixing failures in headless mode
  • preserve existing /goal behavior: goal continuations run first, autonomous continuation is a later fallback

Headless / Verifiers

Testing

  • npm --prefix packages/coding-agent test -- suite/agent-session-autonomous.test.ts args.test.ts
  • ./node_modules/.bin/tsgo --noEmit
  • npm run check
  • uv run --isolated --no-project --with-editable /Users/milkkarten/Research/verifiers-pr1828 --with-editable /Users/milkkarten/Research/prime-agent-headless-v1 --with pytest python -m pytest tests/test_import.py -q
  • ProgramBench typed dry-run wrote validated config with autonomous = true, autonomous_gates = ["./verify-public.sh"], labels = ["programbench"], and X-Prime-Team-ID present

Notes

initialImages?: ImageContent[];
}

function latestGateAttempt(status: AgentAutonomousStatus): number {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium modes/print-mode.ts:29

latestGateAttempt() computes the maximum across all entries in status.gateAttempts, including stale retry counts for commands other than status.lastGateFailure.command. With multiple quality-gate commands, a later command can fail several times and then an earlier command can fail on its first retry. In that state lastGateFailure.attempt is 1, but latestGateAttempt() returns the stale higher count from the later command. As a result, waitForPrintModeIdleWithAutonomousGates() sees attempt <= lastPromptedGateAttempt and stops retrying before the new failure is prompted, or shouldContinuePrintModeAutonomousGates() sees the count exceed maxRetries and stops even though the failing command has only been retried once. Consider tracking the attempt count only for the command in lastGateFailure.command instead of taking the maximum across all commands.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @packages/coding-agent/src/modes/print-mode.ts around line 29:

`latestGateAttempt()` computes the maximum across all entries in `status.gateAttempts`, including stale retry counts for commands other than `status.lastGateFailure.command`. With multiple quality-gate commands, a later command can fail several times and then an earlier command can fail on its first retry. In that state `lastGateFailure.attempt` is `1`, but `latestGateAttempt()` returns the stale higher count from the later command. As a result, `waitForPrintModeIdleWithAutonomousGates()` sees `attempt <= lastPromptedGateAttempt` and stops retrying before the new failure is prompted, or `shouldContinuePrintModeAutonomousGates()` sees the count exceed `maxRetries` and stops even though the failing command has only been retried once. Consider tracking the attempt count only for the command in `lastGateFailure.command` instead of taking the maximum across all commands.

);
}

async function waitForPrintModeIdleWithAutonomousGates(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium modes/print-mode.ts:58

waitForPrintModeIdleWithAutonomousGates sends follow-up gate-retry prompts via session.prompt(...) but never increments status.continuationsUsed, so autonomousLimitsReached()'s maxContinuations check never advances during print-mode retries. The --autonomous-max-continuations limit is silently ignored and the loop can continue more times than configured. Consider incrementing continuationsUsed (or routing through nextAutonomousContinuation) before each follow-up prompt.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @packages/coding-agent/src/modes/print-mode.ts around line 58:

`waitForPrintModeIdleWithAutonomousGates` sends follow-up gate-retry prompts via `session.prompt(...)` but never increments `status.continuationsUsed`, so `autonomousLimitsReached()`'s `maxContinuations` check never advances during print-mode retries. The `--autonomous-max-continuations` limit is silently ignored and the loop can continue more times than configured. Consider incrementing `continuationsUsed` (or routing through `nextAutonomousContinuation`) before each follow-up prompt.

return false;
}

if (this._queueAutonomousContinuationForThresholdCompaction(context.message)) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 High core/agent-session.ts:1379

_shouldStopForThresholdCompaction() calls _queueAutonomousContinuationForThresholdCompaction() and returns true, but the same assistant message is processed again in the later agent_end compaction path where _checkCompaction() also calls _queueAutonomousContinuationForThresholdCompaction(). This queues two identical autonomous follow-up user messages and increments state.continuationsUsed twice for a single turn. After compaction, _runPostCompactionContinue() sends both messages, causing duplicate autonomous prompts and premature exhaustion of the continuation limit. Consider gating the autonomous continuation so it is only queued once per assistant message.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @packages/coding-agent/src/core/agent-session.ts around line 1379:

`_shouldStopForThresholdCompaction()` calls `_queueAutonomousContinuationForThresholdCompaction()` and returns `true`, but the same assistant message is processed again in the later `agent_end` compaction path where `_checkCompaction()` also calls `_queueAutonomousContinuationForThresholdCompaction()`. This queues two identical autonomous follow-up user messages and increments `state.continuationsUsed` twice for a single turn. After compaction, `_runPostCompactionContinue()` sends both messages, causing duplicate autonomous prompts and premature exhaustion of the continuation limit. Consider gating the autonomous continuation so it is only queued once per assistant message.

await this.agent.waitForIdle();
await this.agent.prompt(messages);
await this.waitForRetry();
await this._waitForPostCompactionContinuations();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium core/agent-session.ts:2320

prompt() calls await this._waitForPostCompactionContinuations(), but the scheduled continuation path swallows all failures with .catch(() => {}). If an agent_end listener keeps the previous run alive past the hard-coded 100 ms delay, _runPostCompactionContinue() calls this.agent.prompt()/this.agent.continue() while activeRun is still set, which throws Agent is already processing. That exception is silently dropped, so prompt() returns successfully even though the continuation was never executed. Consider awaiting the continuation directly (or propagating its error) instead of scheduling with a fire-and-forget .catch(() => {}), so a still-active run surfaces as a real error rather than a silent no-op.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @packages/coding-agent/src/core/agent-session.ts around line 2320:

`prompt()` calls `await this._waitForPostCompactionContinuations()`, but the scheduled continuation path swallows all failures with `.catch(() => {})`. If an `agent_end` listener keeps the previous run alive past the hard-coded 100 ms delay, `_runPostCompactionContinue()` calls `this.agent.prompt()`/`this.agent.continue()` while `activeRun` is still set, which throws `Agent is already processing`. That exception is silently dropped, so `prompt()` returns successfully even though the continuation was never executed. Consider awaiting the continuation directly (or propagating its error) instead of scheduling with a fire-and-forget `.catch(() => {})`, so a still-active run surfaces as a real error rather than a silent no-op.

themes: resolveCliPaths(cwd, parsed.themes),
noThemes: parsed.noThemes,
noContextFiles: parsed.noContextFiles,
autonomous: parsed.autonomous

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium src/main.ts:666

runtimeConfigFromArgs only builds the autonomous config object when parsed.autonomous is truthy, so all --autonomous-* CLI flags are discarded whenever the session doesn't start in autonomous mode. For example, --autonomous-max-turns 1 used with interactive mode and then /autonomous on leaves config.autonomous as undefined, so the runtime initializes with default limits and the user's configured overrides are never applied. Consider populating the autonomous object whenever any autonomous* field is set, not just when parsed.autonomous is true.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @packages/coding-agent/src/main.ts around line 666:

`runtimeConfigFromArgs` only builds the `autonomous` config object when `parsed.autonomous` is truthy, so all `--autonomous-*` CLI flags are discarded whenever the session doesn't start in autonomous mode. For example, `--autonomous-max-turns 1` used with interactive mode and then `/autonomous on` leaves `config.autonomous` as `undefined`, so the runtime initializes with default limits and the user's configured overrides are never applied. Consider populating the `autonomous` object whenever any `autonomous*` field is set, not just when `parsed.autonomous` is true.

return "failed";
}
for (const command of state.gates.commands) {
const currentSnapshot = captureGitWorktreeSnapshot(cwd);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium core/autonomous.ts:252

runAutonomousQualityGates captures currentSnapshot before the gate command runs and then stores it in lastGateFailureSnapshot when the gate fails. If the failing gate itself mutates the worktree (e.g. writes a lockfile, formats files, updates fixtures), the stored snapshot reflects the pre-command state. On the next call, captureGitWorktreeSnapshot returns the post-mutation state, so the equality check fails and the guard reruns the gate even though the assistant has not changed anything. This defeats the "do not rerun until the workspace changes" check and can repeatedly execute expensive or side-effecting gates until retries are exhausted. Consider capturing the snapshot after the gate command finishes so that lastGateFailureSnapshot reflects the state the assistant must change before the gate is rerun.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @packages/coding-agent/src/core/autonomous.ts around line 252:

`runAutonomousQualityGates` captures `currentSnapshot` before the gate command runs and then stores it in `lastGateFailureSnapshot` when the gate fails. If the failing gate itself mutates the worktree (e.g. writes a lockfile, formats files, updates fixtures), the stored snapshot reflects the pre-command state. On the next call, `captureGitWorktreeSnapshot` returns the post-mutation state, so the equality check fails and the guard reruns the gate even though the assistant has not changed anything. This defeats the "do not rerun until the workspace changes" check and can repeatedly execute expensive or side-effecting gates until retries are exhausted. Consider capturing the snapshot after the gate command finishes so that `lastGateFailureSnapshot` reflects the state the assistant must change before the gate is rerun.

if (status.status !== 0 || typeof status.stdout !== "string") {
return undefined;
}
const diff = spawnSync("git", ["--no-optional-locks", "diff", "--no-ext-diff", "--binary", "HEAD", ...pathspec], {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium core/autonomous.ts:347

captureGitWorktreeSnapshot sets diff to "" whenever git diff HEAD fails. In a fresh repository with no commits, HEAD is unborn so git diff HEAD fails on every snapshot. Because the status output only records pathnames (?? path) and not file contents, editing an existing untracked file after autonomous mode starts does not change either status or diff, so hasGitWorktreeChangedSinceBaseline returns false for real edits. This makes autonomous mode treat genuine work as "no terminal evidence" and keep issuing continuation prompts in fresh repos until a limit is hit. Consider falling back to a diff of the working tree against the index (or an empty tree) when HEAD is unborn so untracked-file edits are reflected in the snapshot.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @packages/coding-agent/src/core/autonomous.ts around line 347:

`captureGitWorktreeSnapshot` sets `diff` to `""` whenever `git diff HEAD` fails. In a fresh repository with no commits, `HEAD` is unborn so `git diff HEAD` fails on every snapshot. Because the `status` output only records pathnames (`?? path`) and not file contents, editing an existing untracked file after autonomous mode starts does not change either `status` or `diff`, so `hasGitWorktreeChangedSinceBaseline` returns `false` for real edits. This makes autonomous mode treat genuine work as "no terminal evidence" and keep issuing continuation prompts in fresh repos until a limit is hit. Consider falling back to a diff of the working tree against the index (or an empty tree) when `HEAD` is unborn so untracked-file edits are reflected in the snapshot.

@sethkarten sethkarten mentioned this pull request Jun 29, 2026
@sethkarten

Copy link
Copy Markdown
Contributor Author

Closing as superseded by #278. The feature/autonomous-eval branch contains this PR's head and folds the autonomous continuation work into the Autonomous Eval integration PR; continue review there.

@sethkarten sethkarten closed this Jul 8, 2026
@kevinjosethomas
kevinjosethomas deleted the feature/autonomous-user-sim branch July 16, 2026 23:52
zhengr pushed a commit to zhengr/prime-agent that referenced this pull request Aug 8, 2026
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.

1 participant