feat(agent-core-v2): add full compaction to the human agent domain - #3703
Conversation
|
commit: |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 7acab6ad6e
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| actor.on('compaction.completed', (event) => { | ||
| deps.onEvent?.(event); | ||
| record(compactionCompleted({ agentId: deps.agentId, branch: event.branchId })); | ||
| lastCompactedTokens = event.stats.tokensBefore; |
There was a problem hiding this comment.
Compare budget progress using the same token total
When the system prompt or tool schemas keep the request above the trigger after compaction, budgetExceeded() compares that request-inclusive total with lastCompactedTokens, but this assignment records tokensBefore, which is calculated from history alone. With short/stable summaries and fixed overhead large enough to exceed the threshold, every continuation triggers another successful compaction, causing an unbounded summary/branch-switch loop without sending an ordinary LLM request. Record and compare equivalent request-inclusive totals, or terminate when compaction cannot reduce the request below the budget.
Useful? React with 👍 / 👎.
| request: context.input.request, | ||
| }), | ||
| onDone: { target: 'thinking' }, | ||
| onError: { target: 'done' }, |
There was a problem hiding this comment.
Do not mark blocked steps done before compaction succeeds
When the budget hook rejects, this transition completes the turn as done with no response while compaction is only scheduled asynchronously. If the automatic compaction then fails—for example because the summary request times out—the cancellation path merely unpauses the agent; the initiating prompt has already been drained into history, there is no pending work, and no new turn starts, so the user receives neither an answer nor a turn failure. Preserve/retry the blocked turn or propagate the hook failure unless compaction successfully schedules its continuation.
Useful? React with 👍 / 👎.
| throw new CompactError('unknown-agent', `unknown agent: '${deps.agentId}'`); | ||
| } | ||
| deps.actor.send({ type: 'input.pause' }); | ||
| const waiting = waitFor(deps.actor, (s) => s.matches('idle'), { timeout: PAUSE_TIMEOUT_MS }); |
There was a problem hiding this comment.
Wait for background tools before taking the snapshot
When a detached tool is still running, the agent is in idle.waiting, which still satisfies matches('idle'), so compaction proceeds immediately instead of reaching true quiescence. The later store reset aborts the agent scope and clears context.background; if the tool completes during summarization, its queued notification is also discarded by the reset. This silently cancels or loses background work, so the snapshot should wait for idle.ready/an empty background set or explicitly preserve those tasks and notifications.
Useful? React with 👍 / 👎.
| history: state.history, | ||
| queue: state.queue, |
There was a problem hiding this comment.
Carry pending reminders into the compacted branch
When an agent with existing history has a keyed reminder pending while idle, the snapshot retains only history and queue even though reminders are intentionally stored until the next turn drain. buildCompactionSeed() therefore emits no input.reminded events, and resetting onto the fresh branch initializes the reminder slice empty, silently removing plugin or system guidance before it is delivered. Include the snapshot's reminder state in the branch seed, as is already done for queued input.
Useful? React with 👍 / 👎.
7acab6a to
28ab86a
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 28ab86a74f
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| deps.actor.on('turn.aborting', () => { | ||
| active?.actor.send({ type: 'cancel', cause: 'user-abort' }); | ||
| }), |
There was a problem hiding this comment.
Cancel compaction when abort arrives after quiescence
When an abort is sent after quiescing has placed the agent in idle—for example while a slow summary request is running—agent/machine.ts ignores input.abort because it only emits turn.aborting from running.active. Consequently this subscription never cancels the compaction, and the request may run until its 120-second timeout and still switch branches despite the user's abort. Route aborts to the active compaction independently of whether an agent turn is still running.
Useful? React with 👍 / 👎.
| dispose: () => { | ||
| active?.actor.send({ type: 'cancel', cause: 'cancelled' }); | ||
| for (const subscription of subscriptions) { | ||
| subscription.unsubscribe(); | ||
| } |
There was a problem hiding this comment.
Prevent queued compactions from running after dispose
If onBeforeStep has already queued run('budget'), or an active run has populated pendingAuto, calling dispose() does not invalidate either scheduled run. The microtask can therefore start a new compaction after disposal, and cancelling the active actor can itself call firePending() and queue another run. This may summarize or switch the agent's branch after its owning controller has been torn down; mark the controller disposed and make run/firePending discard pending work.
Useful? React with 👍 / 👎.
Related Issue
Internal feature task (feat-177) — no external issue.
Problem
The event-sourced "human" agent domain (agent-core-v2
src/human) has no context compaction: once history grows past the model window, turns fail with context overflow and there is no recovery path. The legacy domain (src/agent/fullCompaction) has full compaction, but its implementation is tied to the old loop/context-memory services and cannot drive the event-sourced agent.What changed
Compaction orchestration — new
human/compaction/module. An external controller (createCompactionController) drives compaction through the agent machine's control surface: pause the agent so the active turn ends at a step boundary, snapshot the event-sourced store, run a summary turn, then switch the agent onto a fresh parent-less branch seeded with kept user messages (head/elision/tail selection ported from the legacy handoff), the summary block, and un-consumed queued inputs. Turn ids stay continuous across the switch, and undo cannot cross the compaction point (topological guard). The legacy production path is untouched, and the controller is not wired into production assembly yet (opt-in).Compaction run as an xstate machine. Each run is an explicit machine (
quiescing → summarizing → switching → resuming → completed/cancelled) whose transitions produce the observability surface the legacy domain had: observable events (compaction.started/blocked/cancelled/completed— cancels and failures both surface ascancelledwith acause, fixing the semantic inversion), durable lifecycle records in the_sessionlog (compaction.started/completed/cancelled), astatus()getter (the legacycompactingequivalent), anonWillCompacthook (withtokenCount), and rich terminal-event payloads (originTurnId, tokens before/after, summary usage/trace id/attempts/dropped count) so assembly can map telemetry (compaction_finished/failed/cancel) without the module depending on a telemetry client.Cancellation and interruption.
cancel()aborts a run while quiescing/summarizing (switching/resuming is the point of no return); a user abort (turn.aborting) cascades to cancel the compaction and leaves the agent paused — inputs and notifications accumulate and no new turn starts until an explicitinput.continue. Inputs submitted/steered/notified during summarization are validated (input.*-only delta, the legacyhistorySafeToCompactsemantics) and replayed onto the new branch instead of cancelling the compaction; non-input writes still cancel it as drift.Budget trigger before the step. The turn machine gains a generic
onBeforeStephook (newgatingstate ahead of every step); the controller's implementation estimates request tokens (system prompt and tools included) and, when over budget, ends the turn before the over-budget request is sent, then compacts and resumes — the legacybeforeStep shouldBlockinterception point (the legacy in-turn retry becomes end-turn + resume, same shape as the overflow path). Overflow recovery is driven byturn.failedwith an attempt cap.Agent primitives and store support.
input.pause/input.continueon the agent machine (pause ends the turn at the step boundary and gates idle draining; continue resumes pending work or revives a tool-chain-ending history);SessionStores.switchBranch(parent-less branch + seed dispatch + reset +agent.switched, generalized from undo);agent.switchedgains optional numericstats.Tests: new controller suite (8 cases — manual, step-boundary pause with queued-input survival, input merge during summarization (submit + steer), cancel API, abort cascade with paused accumulation, drift guard, budget gate before an over-budget step, overflow attempt cap), agent machine pause/continue cases,
switchBranchstore cases. Legacysrc/agentbehavior is unchanged.Checklist
gen-changesetsskill, or this PR needs no changeset — the feature is not wired into the shipped CLI yet and is not user-perceivable.gen-docsskill, or this PR needs no doc update (internal mechanics only).