Skip to content

🐛 harness: log the agent's closing message and goose's error.data - #170

Merged
djzager merged 2 commits into
konveyor:mainfrom
ibolton336:fix/harness-agent-text
Aug 26, 2026
Merged

djzager merged 2 commits into
konveyor:mainfrom
ibolton336:fix/harness-agent-text

Conversation

@ibolton336

Copy link
Copy Markdown
Member

What

Two pod-log gaps in the harness, both found on the same day from one teammate's runs:

  • prompt error -32603: Internal error with no reason. goose puts the cause in JSON-RPC error.data (e.g. Error getting agent reply: Provider not set); the harness parsed and dropped it. RPCError.Error() now appends Detail() (string data unquoted, other JSON compacted) and SendPrompt/WSClient.Call wrap it with %w, so the log reads
    prompt error -32603: Internal error — Error getting agent reply: Provider not set
    and callers can errors.As on the *RPCError. The partial PromptResult is returned alongside the error.
  • The model's words were never recorded. Assistant text went into PromptResult.Chunks and nowhere else; the live transcript is in the tee and dies with the pod. A run whose result was text (an answer, a question back to the user, a summary) ended stage succeeded — no changes to push with no trace. The harness now keeps the closing message — the agent messages after the last tool_call (or the latest human steer), one entry per goose message — and logs it after the turn (INFO, truncated at 4000 runes, secrets redacted). goose's own status notices (_goose/unstable/session/update status_message/notice, e.g. Unable to continue: Context limit still exceeded after compaction) are recorded and logged too.
  • Provider failures hidden as prose. goose 1.45 reports provider errors as assistant messages (Ran into this error…, The provider refused this request., …Please resend your message to try again., The model returned an empty response.) and ends the turn normally, so a run that never reached the model reported success. When the closing message is one of those (matched anchored to the message, so an agent quoting the phrase is not mistaken for a failure), it is logged as WARN and attached viewers get a notice. Credits exhaustion is not in the list: over ACP goose turns it into the session/prompt RPC error, which the first change now prints.
  • Max turns is now decided before the tripping tool_call is handled, so the narration that introduced it survives as the closing text instead of an always-empty "none".
  • Redaction. Logging model prose opens a channel that did not exist; goose inherits the harness's environment. Every credential-looking env value (SECRET|TOKEN|PASSWORD|API_KEY|ACCESS_KEY|CREDENTIALS names, ≥ 8 chars) plus the API key, hub token, ACP secret and git token the harness holds are replaced with [redacted] in the closing message, notices and prompt errors.

Exit status is unchanged on purpose: a provider failure rendered as prose still exits 0 (README bullet updated to say so). Flipping that belongs with the Succeeded condition work in #119.

Verification

  • gofmt, go vet, go test ./... green. New tests: TestRPCErrorCarriesData (jsonrpc_test.go), TestSendPromptErrorIncludesGooseDetail (partial result + errors.As), TestSendPromptMaxTurnsKeepsClosingText, TestPromptResultFinalMessageFollowsLastToolCall (message boundaries, steer reset), TestPromptResultKeepsGooseNotices, TestLooksLikeProviderError (anchored, per-message), redactor tests.
  • E2E on a minikube rig (agentic-controller + Agent Sandbox + Bedrock gateway), agent-base built from this branch, runs created through the Hub API with the console's payload:
    • normal run → pod log ends with agent closing message: followed by the model's text;
    • Gateway with a nonexistent model id → after goose's six retries: WARN agent closing message is goose's provider-error text … ValidationException … The provided model identifier is invalid. Before: only stage succeeded — no changes to push;
    • run with a fake TEST_API_KEY_SECRET in spec.env and instructions to print it → [redacted] in the pod log.
  • Wire shapes checked against goose v1.45.0 (crates/goose/src/agents/agent.rs reply loop, crates/goose/src/acp/server.rs message_update_meta, send_status_message_update, prompt_error_from_message_content).

Context

Seen 2026-08-21: a console-created run on a Vertex gateway whose Secret lacked GCP_PROJECT_ID died at the first prompt with a bare -32603; and a run whose instructions asked the model a question ended Succeeded with nothing to show — the model had answered "Round 1/10: Tell me something." and stopped.

🤖 Generated with Claude Code

Two things a dead or empty run left behind were nothing:

1. On a session/prompt failure goose keeps the JSON-RPC message at the
   boilerplate ("Internal error") and puts the cause in error.data
   ("Error getting agent reply: Provider not set"). The harness printed
   code and message only, so the pod log said `prompt error -32603:
   Internal error` and the reason died with the pod. RPCError.Error()
   now appends the data field and both SendPrompt and WSClient.Call
   format the error with it.

2. Assistant text was collected into PromptResult.Chunks and discarded.
   The live transcript lives in the tee and dies with the pod, so a run
   whose only output was text ended `stage succeeded — no changes to
   push` with no record of what the model said. That includes goose's
   provider failures: the reply loop turns them into assistant prose
   ("Ran into this error: …") and ends the turn normally, so a run that
   never reached the model reported success. The harness now tracks the
   closing message (text after the last tool call or the latest steer,
   one entry per goose message) and writes it to the pod log after the
   turn — WARN plus a viewer notice when it is goose's provider-failure
   text (anchored to the message: "Ran into this error", "The provider
   refused this request.", "The model returned an empty response.",
   "…Please resend your message to try again."), INFO otherwise,
   truncated at 4000 runes. goose's status notices ("Unable to continue:
   Context limit still exceeded after compaction") travel on its custom
   update method and are recorded with the result too. The partial
   result now survives an RPC error, and max turns is decided before the
   tripping tool call so the narration that introduced it is kept.

   Exit status is unchanged on purpose: a provider failure rendered as
   prose still exits 0 (README says so); flipping that belongs with the
   Succeeded condition work in konveyor#119.

Seen 2026-08-21: a console-created run on a Vertex gateway whose Secret
lacked GCP_PROJECT_ID died at the first prompt with a bare -32603; and a
run whose instructions asked the model a question ended Succeeded with
nothing to show — the model had answered "Round 1/10: Tell me something."
and stopped.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: ibolton336 <ibolton@redhat.com>
@ibolton336
ibolton336 force-pushed the fix/harness-agent-text branch from 8ae284e to 4a3a2fc Compare August 21, 2026 19:16
@coderabbitai

coderabbitai Bot commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: a1405c12-617f-4192-9662-7f7d681aa7c6


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

A review probe showed goose can stream ordinary text and then emit its
"Ran into this error…" message in the same post-tool-call segment. The
per-message split covers that when chunks carry a message id (goose 1.45
stamps one on every chunk); when they do not, the failure still starts a
line, so prefixes are now also matched at line starts. The WARN and the
viewer notice say the call *may* have failed: a refusal is not a failed
call, and the harness cannot see further than goose's text.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: ibolton336 <ibolton@redhat.com>
@djzager

djzager commented Aug 24, 2026

Copy link
Copy Markdown
Member

Surfacing goose's error.data and the closing message is exactly what was missing for debugging dead runs — nice.

Agreed on keeping exit status out of scope here; no new issue needed since the deferred half is already tracked:

The controller half is landing in #169 (ADR 0018): it reads the container exit code (0/1/2 → Succeeded/Failed/LimitReached) and the termination message into status.terminationData. So once #118 emits exit 2 + the blob, #169 consumes it end-to-end — nothing to change in this PR.

One note for whoever picks up #118: the harness currently only does os.Exit(1), so LimitReached is unreachable until the exit-2 path lands. Worth sequencing #118 after #169 so it implements against the merged contract.

@djzager
djzager merged commit 01ee369 into konveyor:main Aug 26, 2026
22 checks passed
ibolton336 added a commit that referenced this pull request Sep 15, 2026
## Problem

The plan ladder the harness publishes to viewers (rendered in the
console's session panel) has three rungs. Two of them were fixed text
for every run: **"Prepare workspace: clone, branch, grounding data"**
and **"Agent works the stage task"**. Only the third named anything
concrete (the push branch). The push tool calls viewers see as "Harness:
git push (final)" did not say where they pushed. And the middle rung was
one spinner for the entire agent turn, which can be minutes and hundreds
of tool calls.

## Change

Build the rungs from what the harness already has in hand, and keep the
middle one moving.

**First rung** (commit 2): repository as host and path with credentials
and `.git` dropped, the branch, and the number of analysis insights
written. "no analysis insights" when the fetch returned none; nothing
about insights when the run has no skills and never fetched them.

```
Prepare workspace: github.com/konveyor/coolstore on branch migration-1789401169, 49 analysis insights
```

**Middle rung** (commits 1 and 3): workflow stage position when there is
one, the first non-empty line of the stage instructions (falling back to
the agent prompt) stripped of Markdown markers and cut to 80 runes, the
model, and the turn budget. Before the prompt is sent it reads `up to 40
turns`; once running, the ladder is re-emitted on every turn with
progress:

```
Stage 2 of 3 — agent works the task: “Fix the findings from the assess stage.” (gemini-2.5-pro, turn 12 of 200)
```

Stage text is rendered with parameter values substituted, so the excerpt
goes through the same redactor the closing-message path uses (#170).
"Turn" is the measure the harness already reports as `TurnsUsed` and
compares against the native limit in `classifyOutcome`: one per
`tool_call` notification. The handoff prompt continues the count from
where the primary stopped.

**Push titles** (commit 2): `git push to branch <branch> (final)` and
`(auto-commit watcher)`.

**Plumbing** (commit 3):
- `SessionClient.SetTurnHandler` fires from `SendPrompt`'s loop each
time the count advances; `runStage` re-emits the ladder from it.
- Re-emitting per turn would fill the tee's 32-frame replay ring and
evict the push and outcome frames a late viewer needs, so the ring now
keys plan frames: a new ladder replaces the previous one instead of
stacking. Unkeyed frames accumulate as before.
- `fetchAndWriteAnalysis` returns the count it already logs.
`workflowStagePosition` is split out of `isIntermediateWorkflowStage` so
both read the stage metadata the same way; an index past the count now
counts as invalid there too, which changes nothing for the intermediate
check.

## Verification

- `go build`, `go vet`, `gofmt -l` clean in `harness/`; full `go test
./...` in `harness/` passes.
- New tests: the rung text across standalone/staged runs, prompt
fallback, truncation, empty case, invalid stage metadata, secret
masking, and progress wording with and without a budget; the prep rung's
credential stripping, `.git` trimming, zero vs. unfetched insights and
unparseable URL; the turn handler firing once per `tool_call` with the
running count and not for other updates (demux-server test); and the
replay ring keeping only the latest plan while unkeyed frames
accumulate.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Signed-off-by: ibolton336 <ibolton@redhat.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
djzager pushed a commit that referenced this pull request Sep 16, 2026
… says why (#232)

Closes #231.

## Problem

goose renders a rejected model call as assistant prose and ends the turn
normally:

```
Ran into this error: Server error: Failed to call Bedrock: … UnrecognizedClientException …
"The security token included in the request is invalid." …
```

The harness recognised that text since #170 but only logged a WARN and
told viewers to *"see the pod log"*. Exit 0, `stopReason=end_turn`, run
**Succeeded** in green, Gateway **Ready** (Bedrock cannot be probed,
#222). On the demo cluster today three runs died this way on a rotated
AWS key and were read as a broken plan ladder, because nothing anyone
could see said otherwise.

## Change

**Harness**

- `classifyOutcome` takes `providerError` (the existing
`PromptResult.ClosingProviderError()`). With `TurnsUsed == 0` the stage
is `outcomeFailed`: the model never worked, so nothing succeeded. A
provider error *after* real work keeps the turn's outcome — that is
#129's broader contract — but still gets the notice below.
- The termination log's `stopReason` becomes `provider error: <the
provider's error line>` (redacted, cut at 1000 runes). The line is the
failure itself: `acp.ProviderErrorText` skips narration goose streamed
ahead of it (*"Here is the plan:"* … *"Ran into this error: …"*), and
`LooksLikeProviderError` is built on it, so detection and selection
cannot drift. The cap is sized for #231's real line: goose renders the
AWS SDK error with Debug formatting, ~500 runes with *"The security
token included in the request is invalid."* past rune 160, which a
200-rune cut lost.
- The viewer notice quotes the same line — *"the model provider rejected
the call — no work was done: Ran into this error: … security token … is
invalid."* — instead of pointing at a pod log a console user cannot
open. The stage-end notice and the returned error name it too.

**Controller**

Unchanged. The termination blob stays opaque to the controller (ADR
0011, ADR 0018, `CONTEXT.md`): it is stored verbatim on
`status.terminationData`, and `setTerminalOutcome` copies the
termination message onto the `Succeeded=False` condition as before.
Showing `terminationData.stopReason` beside the phase instead of the raw
blob belongs in the UI, which knows the harness schema — a tackle2-ui
follow-up.

Net effect: the run is **Failed** instead of Succeeded, and the
provider's error line — the sentence that tells the operator to rotate
the Gateway credential — is in the viewer notice and in the termination
blob's `stopReason`, which the console shows raw beside the phase until
the UI renders it.

Exit-code contract (ADR 0011) is unchanged: this is an exit-1 failure,
not a new code. #170's *"exit status is unchanged on purpose … belongs
with the Succeeded condition work in #119"* deferral is what this
resolves; #119 landed in #169.

## Verification

- `gofmt`, `go vet`, `go test ./...` green in `harness/`; `go vet` plus
`TestSetTerminalOutcome`, `TestPodTerminationMessage`,
`TestTerminationDataFromPod` green in `internal/controller/`.
- New cases: `TestClassifyOutcome` — provider error with no turns →
failed; after 7 turns → succeeded; at the native limit → limitReached
still wins. `TestProviderErrorText` — the failure without the narration
ahead of it, in both of goose's forms; a quoted phrase is not a failure.
`TestProviderErrorSummary` — narration in the same or an earlier
message, the trailer form, #231's Bedrock line kept whole, the 1000-rune
cut.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Signed-off-by: ibolton336 <ibolton@redhat.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
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.

2 participants