Agentloop summary at bottom of issue description #476

Closed
opened 2026-06-06 14:25:11 +00:00 by guettli · 3 comments
guettli commented 2026-06-06 14:25:11 +00:00 (Migrated from codeberg.org)

I want a Agentloop summary at bottom of issue description.

On each tick create a ensure there is a summary at the bottom of the issue description.

Seperate it from user content with



The summary should list all runs, and the used tokens.

Think about additional data which might be useful. Like start/end times.

Only update the summary if it has changed.

I want a Agentloop summary at bottom of issue description. On each tick create a ensure there is a summary at the bottom of the issue description. Seperate it from user content with --- --- The summary should list all runs, and the used tokens. Think about additional data which might be useful. Like start/end times. Only update the summary if it has changed.
guettlibot commented 2026-06-06 14:35:03 +00:00 (Migrated from codeberg.org)

Now I have everything I need. Here is the implementation plan:


Implementation Plan for Issue Summary at Bottom of Issue Description

Overview

On each tick, after state transitions (run start, finish, fail), append an agentloop summary section to the issue description, separated from user content by ---\n---. Update only when the content has changed.


Step 1 — Add UpdateIssueBody to the Tracker interface

File: internal/tracker/tracker.go

Add to the Tracker interface:

UpdateIssueBody(ctx context.Context, number int, body string) error

Implement on Client using forges.UpdateIssueOpts{Body: &body} (same Issues().Update call already used by SetLabels). Update the fake tracker in tests accordingly.


Step 2 — Parse token usage from ACP output

File: internal/agent/result.go

Extend rpcMsg.Result to capture usage:

Result *struct {
    StopReason string `json:"stopReason"`
    Usage      struct {
        InputTokens  int `json:"inputTokens"`
        OutputTokens int `json:"outputTokens"`
    } `json:"usage"`
} `json:"result"`

Add InputTokens int and OutputTokens int fields to Result and populate them when sawResult is set. If the ACP protocol doesn't expose usage, the fields stay zero and display as .


Step 3 — Persist per-run outcome data

File: internal/agent/result.go (or new internal/meta/rundata.go)

After finishOne resolves a result, write run-stats.json into the run directory:

{"endTime": "2026-06-06T10:05:00Z", "inputTokens": 1234, "outputTokens": 567, "success": true}

Start time is already encoded in the directory name (run-20060102T150405.000000000Z). Runs without a stats file (in-progress or pre-feature) show in the table.


Step 4 — Implement summary rendering

New file: internal/loop/summary.go

const summaryDelimiter = "\n\n---\n---\n"
const summaryMarker    = "<!-- agentloop-summary -->"
  • collectRuns(store *meta.Store) ([]runSummary, error) — reads all run-*/run-stats.json files and parses start time from directory name.
  • renderSummarySection(runs []runSummary) string — renders a Markdown table:
<!-- agentloop-summary -->
**Agentloop runs**

| # | Started (UTC)       | Ended (UTC)         | Status  | Tokens in | Tokens out |
|---|---------------------|---------------------|---------|-----------|------------|
| 1 | 2026-06-06 10:00:00 | 2026-06-06 10:05:12 | success |     1 234 |        567 |
| 2 | 2026-06-06 11:00:00 | —                   | running |         — |          — |

Step 5 — Add updateIssueSummary to Runner

File: internal/loop/loop.go (or summary.go)

func (r *Runner) updateIssueSummary(ctx context.Context, issue forges.Issue) error {
    runs, err := collectRuns(r.store(issue.Number))
    if err != nil || len(runs) == 0 {
        return err
    }
    userBody := issue.Body
    if idx := strings.Index(userBody, summaryDelimiter); idx >= 0 {
        userBody = userBody[:idx]
    }
    newBody := userBody + summaryDelimiter + renderSummarySection(runs)
    if newBody == issue.Body {
        return nil
    }
    return r.Tracker.UpdateIssueBody(ctx, issue.Number, newBody)
}

The issue object passed in must carry the current body from the tracker to make change detection accurate.


Step 6 — Wire into tick state transitions

Call updateIssueSummary after:

  • finishOne — after SetLabels succeeds (use the issue already in scope)
  • startOne — after the agent is launched; call r.Tracker.GetIssue first so the body reflects the newly created run directory
  • fail — same as finishOne

This covers "on each tick" because every issue that changes state within a tick gets its description updated exactly once per tick transition.


Key design decisions

Decision Rationale
strings.Index(body, summaryDelimiter) for stripping Simple; no regex; sentinel <!-- agentloop-summary --> guards against false positives in user content
Double ---\n--- separator Matches the issue request; renders as two <hr> visually separating user content
Write run-stats.json at finalization time meta.Store already has the run dir; no new keys needed; file absence = run still in progress
Update only on state transitions, not a separate full-scan pass Avoids N×M tracker API calls per tick; every run start/finish already holds the issue object
Graceful degradation for missing stats Runs before this feature was added show — table renders without error
Now I have everything I need. Here is the implementation plan: --- ## Implementation Plan for Issue Summary at Bottom of Issue Description ### Overview On each tick, after state transitions (run start, finish, fail), append an agentloop summary section to the issue description, separated from user content by `---\n---`. Update only when the content has changed. --- ### Step 1 — Add `UpdateIssueBody` to the `Tracker` interface **File:** `internal/tracker/tracker.go` Add to the `Tracker` interface: ```go UpdateIssueBody(ctx context.Context, number int, body string) error ``` Implement on `Client` using `forges.UpdateIssueOpts{Body: &body}` (same `Issues().Update` call already used by `SetLabels`). Update the fake tracker in tests accordingly. --- ### Step 2 — Parse token usage from ACP output **File:** `internal/agent/result.go` Extend `rpcMsg.Result` to capture usage: ```go Result *struct { StopReason string `json:"stopReason"` Usage struct { InputTokens int `json:"inputTokens"` OutputTokens int `json:"outputTokens"` } `json:"usage"` } `json:"result"` ``` Add `InputTokens int` and `OutputTokens int` fields to `Result` and populate them when `sawResult` is set. If the ACP protocol doesn't expose usage, the fields stay zero and display as `—`. --- ### Step 3 — Persist per-run outcome data **File:** `internal/agent/result.go` (or new `internal/meta/rundata.go`) After `finishOne` resolves a result, write `run-stats.json` into the run directory: ```json {"endTime": "2026-06-06T10:05:00Z", "inputTokens": 1234, "outputTokens": 567, "success": true} ``` Start time is already encoded in the directory name (`run-20060102T150405.000000000Z`). Runs without a stats file (in-progress or pre-feature) show `—` in the table. --- ### Step 4 — Implement summary rendering **New file:** `internal/loop/summary.go` ``` const summaryDelimiter = "\n\n---\n---\n" const summaryMarker = "<!-- agentloop-summary -->" ``` - `collectRuns(store *meta.Store) ([]runSummary, error)` — reads all `run-*/run-stats.json` files and parses start time from directory name. - `renderSummarySection(runs []runSummary) string` — renders a Markdown table: ``` <!-- agentloop-summary --> **Agentloop runs** | # | Started (UTC) | Ended (UTC) | Status | Tokens in | Tokens out | |---|---------------------|---------------------|---------|-----------|------------| | 1 | 2026-06-06 10:00:00 | 2026-06-06 10:05:12 | success | 1 234 | 567 | | 2 | 2026-06-06 11:00:00 | — | running | — | — | ``` --- ### Step 5 — Add `updateIssueSummary` to `Runner` **File:** `internal/loop/loop.go` (or `summary.go`) ```go func (r *Runner) updateIssueSummary(ctx context.Context, issue forges.Issue) error { runs, err := collectRuns(r.store(issue.Number)) if err != nil || len(runs) == 0 { return err } userBody := issue.Body if idx := strings.Index(userBody, summaryDelimiter); idx >= 0 { userBody = userBody[:idx] } newBody := userBody + summaryDelimiter + renderSummarySection(runs) if newBody == issue.Body { return nil } return r.Tracker.UpdateIssueBody(ctx, issue.Number, newBody) } ``` The `issue` object passed in must carry the **current** body from the tracker to make change detection accurate. --- ### Step 6 — Wire into tick state transitions Call `updateIssueSummary` after: - **`finishOne`** — after `SetLabels` succeeds (use the `issue` already in scope) - **`startOne`** — after the agent is launched; call `r.Tracker.GetIssue` first so the body reflects the newly created run directory - **`fail`** — same as `finishOne` This covers "on each tick" because every issue that changes state within a tick gets its description updated exactly once per tick transition. --- ### Key design decisions | Decision | Rationale | |---|---| | `strings.Index(body, summaryDelimiter)` for stripping | Simple; no regex; sentinel `<!-- agentloop-summary -->` guards against false positives in user content | | Double `---\n---` separator | Matches the issue request; renders as two `<hr>` visually separating user content | | Write `run-stats.json` at finalization time | `meta.Store` already has the run dir; no new keys needed; file absence = run still in progress | | Update only on state transitions, not a separate full-scan pass | Avoids N×M tracker API calls per tick; every run start/finish already holds the `issue` object | | Graceful degradation for missing stats | Runs before this feature was added show `—` — table renders without error |
guettlibot commented 2026-06-06 23:45:17 +00:00 (Migrated from codeberg.org)

agentloop: the agent run failed: Authentication required

agent stderr tail:

[acpx] session agentloop-sharedinbox-issue-476 (9a8d04a0-419e-4f9f-a223-6fc26e188c27) · /data/agentloop/sharedinbox/issues/476 · agent needs reconnect
agentloop: the agent run failed: Authentication required agent stderr tail: ``` [acpx] session agentloop-sharedinbox-issue-476 (9a8d04a0-419e-4f9f-a223-6fc26e188c27) · /data/agentloop/sharedinbox/issues/476 · agent needs reconnect ```
guettli commented 2026-06-07 04:51:57 +00:00 (Migrated from codeberg.org)

wrong repo.

wrong repo.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: guettli/sharedinbox#476