Skip to content

feat(harness): export stopReasonToUserMessage helper#60

Merged
khaliqgant merged 1 commit intomainfrom
feat/harness-stop-reason-message
Apr 25, 2026
Merged

feat(harness): export stopReasonToUserMessage helper#60
khaliqgant merged 1 commit intomainfrom
feat/harness-stop-reason-message

Conversation

@kjgbot
Copy link
Copy Markdown

@kjgbot kjgbot commented Apr 25, 2026

Summary

  • Adds stopReasonToUserMessage(stopReason, options?) to @agent-assistant/harness, returning a one-line, end-user-facing string for every documented HarnessStopReason (and a generic fallback for unknown/empty values).
  • Optional canRetry switch flips retryable reasons (max_iterations_reached, max_tool_calls_reached, timeout_reached, model_invalid_response) to retry-language so consumers with a queued fallback path don't dead-end the user.
  • Re-exports the function and StopReasonMessageOptions type from packages/harness/src/index.ts.

Why

Today every harness consumer (sage's slack-runner.ts, cloud's specialist-worker fallback, future CLI surfaces) writes its own ad-hoc switch from result.stopReason → user-visible string. That's the silent-failure footgun: when a consumer forgets to map a new stop reason, the user gets an empty reply.

Centralizing the mapping in @agent-assistant/harness means:

  • One voice across surfaces.
  • Adding a new HarnessStopReason variant only needs one copy update; consumers automatically get a sensible message.
  • Sage's slack-webhook layer can guarantee "never post empty content" by composing this with result.assistantMessage?.text || stopReasonToUserMessage(result.stopReason).

Test plan

  • Typecheck clean — npx tsc -p tsconfig.json --noEmit
  • New unit tests: src/stop-reason-message.test.ts — 8 cases passing
    • every documented stop reason returns non-empty
    • undefined / empty / unknown stop reasons fall through cleanly
    • canRetry flips retryable reasons
    • distinct copy per reason (one allowed collision for max_iterations / max_tool_calls — same UX intentionally)
    • clarification / refusal / cancellation reasons surface their semantic in the copy
  • Full harness suite: 13/14 test files pass, 115 tests pass. The remaining file (byoh-local-proof.test.ts) fails on origin/main too — @agent-assistant/connectivity package entry resolution; unrelated to this change.

Follow-up

A sibling PR will land in AgentWorkforce/sage to use this helper from slack-runner.ts and to add the empty-content guard at slack-webhooks.ts:452 so an empty response.content never produces a silent Slack reply.

…ntly drop turns

Adds a tiny helper that maps every documented HarnessStopReason to a
non-empty, end-user-facing message. Exported from
@agent-assistant/harness so sage's slack-runner, cloud's
specialist-worker fallback, and any future surface share one voice
instead of each shipping its own ad-hoc table — the silent-failure
footgun was when a consumer forgot a stop reason and posted an empty
reply.

Includes an optional `canRetry` switch so callers with a queued
fallback path (e.g. sage's harness→swarm retry) can produce
"retrying with a fallback" wording instead of dead-ending the user.

Tests cover:
- every documented stop reason returns a non-empty string
- undefined / empty / unknown values fall through to a generic message
- canRetry flips retryable reasons to retry-language
- copy is distinct per reason (with one allowed collision for the
  max_iterations / max_tool_calls pair, which share UX intentionally)
@khaliqgant khaliqgant merged commit eae9b3b into main Apr 25, 2026
1 check passed
@khaliqgant khaliqgant deleted the feat/harness-stop-reason-message branch April 25, 2026 17:29
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