Skip to content

feat(hooks): add context_assembled, loop_iteration_start/end observability hooks [claude, human developer oversight]#33915

Open
zeroaltitude wants to merge 31 commits intoopenclaw:mainfrom
zeroaltitude:feat/hook-context-assembled-loop-iteration
Open

feat(hooks): add context_assembled, loop_iteration_start/end observability hooks [claude, human developer oversight]#33915
zeroaltitude wants to merge 31 commits intoopenclaw:mainfrom
zeroaltitude:feat/hook-context-assembled-loop-iteration

Conversation

@zeroaltitude
Copy link
Copy Markdown

@zeroaltitude zeroaltitude commented Mar 4, 2026

Summary

Add three observability-only plugin hooks around the embedded agent loop so plugins can measure:

  • what context is about to be sent to the model (context_assembled)
  • per-iteration lifecycle (loop_iteration_start / loop_iteration_end)

These hooks are void + fire-and-forget (parallel execution, errors logged), so they don't affect control flow or the critical path.

Review focus: src/plugins/types.ts (hook names + event types) and src/agents/pi-embedded-runner/run/attempt.ts (emission sites).

New hooks

Hook Fires Primary use
context_assembled Once per run, after system prompt + effective user prompt + messages are assembled, before first LLM call Context census, token budgeting, audit/forensics
loop_iteration_start At each agent loop turn_start Recursion/iteration depth, per-turn monitoring
loop_iteration_end At each turn_end Tool-call volume, loop termination observability

Event payloads (final)

// context_assembled (fires once; iteration is always 1)
{
  systemPrompt: string;
  prompt: string;              // effective prompt (after hook modifications)
  messages: AgentMessage[];    // snapshot to avoid mutation during async handlers
  messageCount: number;
  imageCount: number;
  iteration: 1;
}

// loop_iteration_start
{
  iteration: number;
  messageCount: number;
  pendingToolResults?: number; // tool results produced by the previous turn
}

// loop_iteration_end
{
  iteration: number;
  toolCallsMade: number;
  newMessagesAdded?: number;   // messageCount delta during this turn
  hasToolResults: boolean;     // informative signal only (not definitive loop terminal)
}

Implementation notes

  • src/plugins/types.ts: adds hook names + event types + handler map entries
  • src/plugins/hooks.ts: adds runContextAssembled, runLoopIterationStart, runLoopIterationEnd (void hooks)
  • src/agents/pi-embedded-runner/run/attempt.ts:
    • Subscribes to turn_start / turn_end to emit iteration hooks
    • Emits context_assembled once before the first LLM call
    • Unsubscribes in cleanup

Tests

src/plugins/hooks.context-loop.test.ts: verifies event shape, parallel execution, and no-op behavior when no handlers are registered.

Compatibility / risk

Additive only. No behavior change unless a plugin registers these hooks; hook errors are swallowed/logged.

AI-Assisted Development 🤖

This PR was developed with AI assistance (Claude/OpenClaw agent "Tank").

  • Degree of testing: Well-tested — unit tests covering event shape, parallel execution, no-op. pnpm check clean.
  • Human oversight: All changes reviewed and directed by Eddie Abrams
  • Understanding confirmed: The contributor understands the embedded agent loop lifecycle and void hook execution model

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Mar 4, 2026

Greptile Summary

This PR adds three void, fire-and-forget observability hooks (context_assembled, loop_iteration_start, loop_iteration_end) around the embedded agent loop, enabling plugins to measure context size, token budget, and per-iteration lifecycle without any impact on control flow. The implementation is additive-only and well-structured.\n\nAfter several review cycles, all previously flagged issues have been addressed: the TDZ / forward-reference concern is fixed by moving hookCtx construction early, willContinue is correctly renamed to hasToolResults, the imageCount field and runId are present in type definitions and test fixtures, newMessagesAdded and pendingToolResults are now populated by the implementation, the hookEventUnsub cleanup is wrapped in a try/catch, and the deep-copy JSDoc is consistent with the structuredClone call.\n\nRemaining issue:\n- The PR moves hookCtx construction out of the try block (line 2632) to fix the forward reference, but does not remove the pre-existing inner hookCtx declaration at line ~2815. The two declarations are currently identical, but the shadowing creates a maintenance hazard if one is updated without the other.

Confidence Score: 4/5

Safe to merge after removing the redundant inner hookCtx declaration; all major concerns from prior cycles are resolved.

The PR has converged well through multiple review iterations. All critical and P1 issues are addressed. The one remaining issue — a duplicate hookCtx declaration that shadows the outer one — is a P2 style concern with no runtime impact today. A targeted fix (removing the inner declaration) would bring this to 5/5.

src/agents/pi-embedded-runner/run/attempt.ts — the inner hookCtx declaration at line 2815 should be removed to eliminate shadowing of the outer one added at line 2632.

Important Files Changed

Filename Overview
src/agents/pi-embedded-runner/run/attempt.ts Adds context_assembled emission and turn_start/turn_end subscription for iteration hooks. The new outer hookCtx at line 2632 was not accompanied by removal of the pre-existing inner hookCtx at line 2815, leaving two identical declarations in overlapping scopes.
src/plugins/types.ts Adds PluginHookContextAssembledEvent, PluginHookLoopIterationStartEvent, PluginHookLoopIterationEndEvent types and handler map entries. JSDoc is accurate and consistent with the implementation after prior review iterations.
src/plugins/hooks.ts Adds runContextAssembled, runLoopIterationStart, runLoopIterationEnd void hook runners delegating to runVoidHook. Clean and consistent with existing patterns.
src/plugins/hooks.context-loop.test.ts Unit tests covering event shape, parallel execution, and no-op behavior for all three new hooks. Fixtures include required fields and correct values.
src/agents/pi-embedded-runner/run/types.ts Adds attemptIndex field to EmbeddedRunAttemptParams, threaded through from the outer run loop for use in hook payloads.
src/agents/pi-embedded-runner/run.ts Single-line change passing attemptIndex (runLoopIterations - 1) to the attempt call. Correct zero-based index computation.
Prompt To Fix All With AI
This is a comment left during a code review.
Path: src/agents/pi-embedded-runner/run/attempt.ts
Line: 2815-2823

Comment:
**Redundant `hookCtx` declaration shadows the outer one**

This PR adds a new `hookCtx` at line 2632 (outside the `try` block) to fix the forward-reference issue for the subscription callback — but the original inner `hookCtx` here was not removed. There are now two identical `hookCtx` objects in scope: the outer one used by the subscription and this inner one used by all subsequent hook calls inside the `try` block.

Because they're built from the same `params` values, the duplication is harmless today. But if one is updated (e.g. to add a new field) without updating the other, the iteration hooks and the prompt/LLM hooks will silently diverge. ESLint's `no-shadow` rule would flag this.

The fix is to remove this inner declaration and reference the outer `hookCtx` (line 2632) throughout the `try` block.

How can I resolve this? If you propose a fix, please make it concise.

Reviews (22): Last reviewed commit: "Merge origin/main into feat/hook-context..." | Re-trigger Greptile

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3a2840ff81

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@zeroaltitude zeroaltitude changed the title feat(hooks): add context_assembled, loop_iteration_start/end observability hooks feat(hooks): add context_assembled, loop_iteration_start/end observability hooks [claude, human developer oversight] Mar 4, 2026
zeroaltitude added a commit to zeroaltitude/openclaw that referenced this pull request Mar 4, 2026
…e as approximate

- context_assembled now includes effectivePrompt so plugins see the
  full model input (system prompt + user prompt + messages)
- willContinue documented as heuristic (doesn't account for abort/timeout)

Addresses greptile + codex-connector review on PR openclaw#33915.
zeroaltitude added a commit to zeroaltitude/openclaw that referenced this pull request Mar 4, 2026
…separate PR)

hook-stream-wrapper.ts incorrectly referenced context_assembled which
is defined in feat/hook-context-assembled-loop-iteration (openclaw#33915), not
this branch. Removed cross-branch dependency so CI passes standalone.

Fixes type errors in CI check job.
@zeroaltitude
Copy link
Copy Markdown
Author

All review items addressed:

  • willContinue heuristic (greptile) — Intentionally approximate; documented on the type. turn_end event only exposes toolResults; true continuation decision lives in the loop controller. llm_output hook is the reliable terminal signal.
  • prompt in context_assembled (codex P2) — Already implemented; effectivePrompt is passed as prompt in the event payload. Tested in hooks.context-loop.test.ts.
  • label CI — GitHub API rate limit flake, not a code issue.

9 tests passing. No code changes needed for this round.

zeroaltitude added a commit to zeroaltitude/openclaw that referenced this pull request Mar 4, 2026
The previous name implied the loop would continue, but the field only
reflects whether tool results were present — it cannot account for
abort signals, timeouts, or max-iteration limits evaluated downstream.

Rename to hasToolResults for accuracy. Strengthened JSDoc to direct
plugin authors to llm_output for definitive terminal detection.

Addresses greptile review on PR openclaw#33915.
@openclaw-barnacle openclaw-barnacle bot added agents Agent runtime and tooling size: M labels Mar 4, 2026
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e5ea615c0b

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

zeroaltitude added a commit to zeroaltitude/openclaw that referenced this pull request Mar 4, 2026
…led single-fire

- Move hookCtx declaration before the subscription callback that
  references it, eliminating the forward-reference fragility flagged
  by greptile (was safe due to async events, now safe unconditionally)
- Document that context_assembled fires once per run (iteration: 1)
  and direct plugin authors to loop_iteration_start for per-turn tracking
- Updated type-level JSDoc on PluginHookContextAssembledEvent

Addresses remaining greptile 4/5 → 5/5 concerns on PR openclaw#33915.
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0b591dab35

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@zeroaltitude
Copy link
Copy Markdown
Author

@greptile-apps: Are there other issues that prevent you from giving this a 5/5? I would like to address any shortcomings if possible.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Mar 5, 2026

Looking at the review threads, there are still 4 unresolved issues that would prevent a 5/5:

  1. hookCtx temporal dead zone (attempt.ts:1303) — subscription callback references hookCtx ~90 lines before it's declared. Move the subscription setup after line 1386.

  2. context_assembled fires only once (attempt.ts:1515) — hardcoded iteration: 1 but the hook never fires on subsequent turns. Either document this as first-call-only or move it into the turn loop.

  3. No message snapshot (attempt.ts, P2) — context_assembled passes activeSession.messages by reference, so async handlers can see mutations. Use .slice() when constructing the event.

  4. Variable shadowing (attempt.ts:1505) — inner messagesSnapshot shadows the outer function-scoped variable. Rename to contextMessagesSnapshot.

The other threads show "Attempted fix!" or "Fixed!" responses, but those haven't been marked resolved yet. If you've pushed commits addressing those, let me know and I can verify.

@zeroaltitude
Copy link
Copy Markdown
Author

All 11 review threads resolved. Here's the status of each issue raised:

Fixed in prior commits:

  1. hookCtx temporal dead zone — Moved hookCtx declaration before subscription setup (83ee19f32). Now at line 1275, subscription at 1290.
  2. context_assembled fires only once — By design. JSDoc documents iteration: 1 as "Always 1 — this hook fires once per run before the first LLM call." Per-turn tracking uses loop_iteration_start/end.
  3. No message snapshotactiveSession.messages.slice() added (b8f54b4cf). Variable named contextMessagesSnapshot.
  4. Variable shadowing — Renamed to contextMessagesSnapshot (26c27a213).
  5. Missing imageCount — Added to test fixture (068b113fc).
  6. iteration: 0 → 1 — Fixed in test fixture and assertion (eba041fec).
  7. willContinue renamed — Now hasToolResults with JSDoc documenting it as approximate.
  8. pendingToolResults/newMessagesAdded — Implemented (1525c49fd, b8f54b4cf).
  9. hookEventUnsub try/catch — Wrapped with CRITICAL-level logging (c83d084fd).
  10. Include current promptprompt field added to context_assembled event.
  11. Add imageCount to type — Added to PluginHookContextAssembledEvent.

All fixes verified against current HEAD (c83d084fd). pnpm check clean.

I tidied up the resolved issues @greptile-apps -- please ensure the fixes are sound and re-score.

zeroaltitude added a commit to zeroaltitude/openclaw that referenced this pull request Mar 5, 2026
…memory changes

- Delete hooks.llm-response.test.ts (328 lines) — complete duplicate of
  hooks.extended.test.ts which has 2 extra after_llm_call tests
- Remove hooks.context-loop.test.ts (160 lines) — belongs to PR openclaw#33915
  (feat/hook-context-assembled-loop-iteration), not this PR
- Revert session-memory handler changes (58 lines) — blockSessionSave
  and sessionSaveRedirectPath are a separate feature; will move to own PR

Net: -512 lines, 49 tests remain (zero coverage loss)
@zeroaltitude
Copy link
Copy Markdown
Author

CI/secrets failing due to pnpm-audit-prod on transitive deps (@buape/carbon -> @hono/node-server -> hono); PR does not modify package.json/lockfile. Happy to send a separate dependency bump/override PR if that’s the preferred fix

@bmendonca3
Copy link
Copy Markdown
Contributor

I think context_assembled currently has “once per attempt” semantics rather than “once per run” semantics.

Right now it fires inside runEmbeddedAttempt() before the first LLM call. But the outer embedded runner can re-enter runEmbeddedAttempt() for the same user-visible run on things like overflow-compaction retry / retry loops, and this hook will emit again with iteration: 1 each time.

So a plugin doing audit/census based on the current docs can double-count a single run and will see multiple context_assembled events that all claim to be the first one.

I think this needs one of:

  • move the emission to the outer runner so it is truly once per run, or
  • document it as once per attempt and stop claiming iteration is always the single run-level first context event

As written, the implementation and the contract look slightly out of sync.

@zeroaltitude
Copy link
Copy Markdown
Author

Good catch — fixed in 2cab5b599.

The outer runner (run.ts) can re-enter runEmbeddedAttempt for the same user-visible run on overflow compaction retries, auth retries, etc. Each re-entry was firing context_assembled with iteration: 1, so plugins doing audit/census would double-count.

Fix: Added attemptIndex param (zero-based, from the outer runLoopIterations counter) and guarded context_assembled to only fire when attemptIndex === 0. Retries skip the hook entirely.

This preserves the "once per run" contract documented in the hook — retries are internal implementation detail that plugins shouldn't see.

…semantics

The type comment and hook runner JSDoc still said 'once per run' and
'before the first LLM call'. Updated to reflect the actual behavior:
fires once per attempt, may fire multiple times per run on retries.
The docs referenced runId for run-level deduplication but plugins
couldn't access it — it wasn't in the event or agent context.
Add runId to PluginHookContextAssembledEvent and pass it through.
- Remove misleading iteration field from context_assembled (always 1, confusing)
- Make attemptIndex required through full call chain (types.ts → params → emission)
- Add shallow-copy safety JSDoc on messages array
- Enhance error logging with runId/iteration/attempt context
- Improve context_assembled emission comment for scannability
- Make runId required in PluginHookContextAssembledEvent (always provided,
  JSDoc advises plugins to key on it — optional was misleading)
- Fix runLoopIterationEnd JSDoc: 'includes whether loop will continue' →
  'includes hasToolResults heuristic hint' (not a definitive signal)
- Move module docstring above all imports in test file
- (pendingToolResults/newMessagesAdded already populated — stale comment)
…ription

Counter increments (hookTurnIteration, hookTurnMessageCount) remain
unconditional since loop_iteration_end depends on state set during
turn_start. Only the hookRunner calls are guarded, consistent with
the hasHooks opt-in philosophy used elsewhere.
…case

Mid-turn compaction can reduce messages.length below the turn_start
snapshot, yielding a negative delta. Clamp to 0 since negative message
counts are nonsensical for plugin consumers.
…rovided

Both fields are always populated by the embedded runner but typed
optional to permit future/custom call sites to omit them.
Replace slice() with structuredClone() for the messages snapshot in
context_assembled. slice() only copies the array — individual AgentMessage
objects are shared references. A plugin handler doing redaction/normalization
on event.messages[i] would mutate the live session context before
activeSession.prompt() reads it.

structuredClone() copies both the array and the message objects, making
the hook truly observability-safe.
…ntics

The implementation was updated to use structuredClone() in 81e7b1e
but the JSDoc still described shallow-copy semantics. Updated to
accurately document that both the array and message objects are
independent copies.
…ntext_assembled

1. loop_iteration_start and loop_iteration_end now include runId and
   attemptIndex so handlers can correlate turn events with their parent
   context_assembled event and distinguish retry attempts (which reset
   the iteration counter to 0) from the initial attempt.

2. context_assembled now includes imageMeta (per-image mimeType and
   approximate decoded byte size) alongside imageCount. This lets
   audit/forensics and token-accounting plugins distinguish vision
   turns with different image sets without exposing raw image data
   (which would be expensive to clone and a wider security blast
   radius).
…_assembled

images was passed as a direct reference to imageResult.images, which is
also passed to activeSession.prompt(). A plugin mutating event.images
(e.g. redacting data for logging) would corrupt the actual model input.
messages were already structuredClone'd for this reason; images need the
same treatment. Skips the clone when there are no images to avoid
unnecessary overhead.
@zeroaltitude zeroaltitude force-pushed the feat/hook-context-assembled-loop-iteration branch from 928a9a9 to 7e82c35 Compare March 22, 2026 06:00
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7e82c35cb3

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +2599 to +2603
runId: params.runId,
attemptIndex: params.attemptIndex,
iteration: hookTurnIteration,
messageCount: activeSession.messages.length,
pendingToolResults: hookPendingToolResults,
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Seed pendingToolResults when retries resume from toolResult history

pendingToolResults is initialized to 0 here and only updated after a prior turn_end, so the first loop_iteration_start of every new attempt reports 0. That breaks on the built-in tool-result truncation recovery path in src/agents/pi-embedded-runner/run.ts:1227-1264: we rewrite oversized toolResult entries in place and immediately continue, so the retried runEmbeddedAttempt starts with those tool results already at the tail of session history and the first model turn is about to consume them. Plugins using this hook for retry telemetry or token accounting will undercount that first retried turn unless this counter is seeded from the current session state before subscribing.

Useful? React with 👍 / 👎.

@zeroaltitude zeroaltitude force-pushed the feat/hook-context-assembled-loop-iteration branch from 7e82c35 to a16dd96 Compare March 26, 2026 06:42
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 761fd47c54

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

attemptIndex: params.attemptIndex,
iteration: hookTurnIteration,
messageCount: activeSession.messages.length,
pendingToolResults: hookPendingToolResults,
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Seed pendingToolResults from session state on retries

pendingToolResults is initialized to 0 for every runEmbeddedAttempt, so the first loop_iteration_start event in a retried attempt always reports zero even when the session already ends with toolResult messages. On the overflow-recovery path in run.ts, truncateOversizedToolResultsInSession(...) mutates existing tool results and immediately continues into a new attempt, so that first turn is actually consuming pending tool outputs. This under-reports retry iterations for telemetry/token-budget plugins and makes cross-attempt loop metrics inconsistent.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d0c0174280

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

* or max-iteration limits — those are evaluated by the loop controller after
* this event fires. Use `llm_output` for definitive loop-terminal detection. */
hasToolResults: boolean;
=======
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P0 Badge Remove unresolved merge markers from plugin hook types

This commit introduces raw conflict-marker text in types.ts (===================================================================== and =======) in the middle of exported type declarations, which leaves PluginHookLoopIterationEndEvent unterminated and makes the file invalid TypeScript. Any import of src/plugins/types.ts will fail to parse, so builds/tests and runtime startup paths that load plugin types are blocked until the markers are removed and the type block is closed correctly.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: aa701f95c7

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

// Subscribe for loop iteration tracking hooks
let hookTurnIteration = 0;
let hookTurnMessageCount = 0;
let hookPendingToolResults = 0;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Seed pending tool-results before emitting first loop start

loop_iteration_start always reports pendingToolResults: 0 on the first turn of a new attempt because the counter is initialized to zero and only updated after a prior turn_end. That is incorrect on retry paths that resume from existing toolResult history (for example after truncateOversizedToolResultsInSession(...) in src/agents/pi-embedded-runner/run.ts:874-912, which mutates session state and immediately continues). In those retries, the first model turn is already consuming pending tool results, so plugins using this hook for telemetry/token accounting undercount the first retried iteration.

Useful? React with 👍 / 👎.

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

Labels

agents Agent runtime and tooling size: M

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants