Skip to content

fix(harness): streamline system prompt handling and enhance message selection logic - #284

Merged
sergiofilhowz merged 3 commits into
mainfrom
fix/harness-system-prompt
Jun 18, 2026
Merged

fix(harness): streamline system prompt handling and enhance message selection logic#284
sergiofilhowz merged 3 commits into
mainfrom
fix/harness-system-prompt

Conversation

@sergiofilhowz

@sergiofilhowz sergiofilhowz commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Removed the obsolete system-prompt.ts file, consolidating system prompt assembly directly within the harness.
  • Introduced a new HarnessSendMode type to manage operating modes (plan, ask, agent) for message handling.
  • Updated the real.ts file to utilize the new mode handling, ensuring the harness constructs identity prompts dynamically based on the provided mode.
  • Enhanced the message selection logic in selection.rs to prevent empty message arrays when summarizing history, ensuring at least the last turn is retained.
  • Added tests to verify the new behavior of message selection and system prompt handling, ensuring compliance with the updated logic.

Move system-prompt assembly into the harness. The console now sends mode (plan | ask | agent) instead of building prompts client-side; the harness selects a provider-specific identity prompt (anthropic, openai, kimi, default) and prepends the mode paragraph when system_prompt is omitted.

Fixes related context/turn failures: context-manager no longer compacts history into an empty tail, session messages are fully paginated, and empty-message guards are added in the turn loop and Anthropic provider.

Test plan

  • Console chat in plan/ask/agent modes — verify behavior without system_prompt override
  • harness.send / harness.spawn with explicit system_prompt — override still wins
  • Long session (>50 messages) — recent context and compaction marker load correctly
  • Over-budget compaction — provider still receives at least one message
  • cargo test -p harness -p context-manager -p provider-anthropic

Summary by CodeRabbit

Release Notes

  • New Features

    • Added operating modes (plan, ask, agent) to control request execution behavior
    • Provider-optimized system prompts now assembled in harness for Anthropic, OpenAI, and Kimi models
  • Bug Fixes

    • Prevents empty message arrays in context compaction, ensuring requests never fail with invalid payloads
  • Documentation

    • Updated binary worker setup guide for SDK v0.19.4
    • Added harness system prompt architecture and mode-based instruction documentation

@vercel

vercel Bot commented Jun 17, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
workers Ready Ready Preview, Comment Jun 18, 2026 12:26pm

Request Review

@coderabbitai

coderabbitai Bot commented Jun 17, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@sergiofilhowz, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 50 minutes and 51 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based credits.

🚦 How do rate limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan refill rate.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, the refill rate gradually slows as usage increases. The highest same-day bursts are limited more strictly.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 2bc4333c-a40b-4dd5-8bb2-20fa4ad3c5ba

📥 Commits

Reviewing files that changed from the base of the PR and between 50c670d and 14f54b0.

📒 Files selected for processing (31)
  • console/web/README.md
  • console/web/src/lib/backend/harness-send.ts
  • console/web/src/lib/backend/real.ts
  • console/web/src/lib/backend/system-prompt.ts
  • context-manager/src/core/selection.rs
  • context-manager/tests/features/assemble.feature
  • context-manager/tests/steps/invariant_steps.rs
  • docs/sops/binary-worker.md
  • harness/README.md
  • harness/architecture/integration.md
  • harness/prompts/anthropic.txt
  • harness/prompts/cli.txt
  • harness/prompts/default.txt
  • harness/prompts/gpt.txt
  • harness/prompts/kimi.txt
  • harness/src/clients/session.rs
  • harness/src/functions/send.rs
  • harness/src/functions/spawn.rs
  • harness/src/lib.rs
  • harness/src/prompt/family.rs
  • harness/src/prompt/mod.rs
  • harness/src/prompt/mode.rs
  • harness/src/prompt/tests.rs
  • harness/src/prompt/variants.rs
  • harness/src/subagent.rs
  • harness/src/turn_loop.rs
  • harness/src/types/turn.rs
  • harness/tests/golden/schemas/harness.run.json
  • harness/tests/golden/schemas/harness.send.json
  • harness/tests/golden/schemas/harness.spawn.json
  • provider-anthropic/src/stream_fn.rs
📝 Walkthrough

Walkthrough

Moves system-prompt assembly from the console into a new harness/src/prompt module that selects provider-specific identity prompts and prepends mode paragraphs at send time. Adds a Mode enum (plan|ask|agent) propagated through SendOptions, SpawnOptions, TurnOptions, and JSON schemas. Removes console/web/src/lib/backend/system-prompt.ts. Adds empty-messages safety guards in context-manager selection, turn_loop, and provider-anthropic. Rewrites session message fetching to paginate. Updates binary-worker docs to iii-sdk v0.19.4.

Changes

Harness system-prompt module and console migration

Layer / File(s) Summary
Prompt module: Mode, family, variants, build/resolve
harness/src/lib.rs, harness/src/prompt/mode.rs, harness/src/prompt/family.rs, harness/src/prompt/variants.rs, harness/src/prompt/mod.rs
Declares pub mod prompt; adds Mode enum with paragraph; adds PromptFamily enum with provider→family routing; exports four prompt-body constants via include_str!; implements build_system_prompt and resolve_system_prompt.
Provider prompt text files
harness/prompts/anthropic.txt, harness/prompts/gpt.txt, harness/prompts/kimi.txt, harness/prompts/default.txt, harness/prompts/cli.txt
Adds four provider-specific and one CLI-variant agent-worker prompt texts covering identity, discovery workflow, tool-call payload rules, error handling, worker installation, SDK authoring, HTTP constraints, and security rules.
Prompt module test suite
harness/src/prompt/tests.rs
344-line test suite covering resolve_system_prompt overrides, default prompt content invariants, registry allowlist, mode-prefix ordering, provider family routing, cross-variant shared invariants, capability ladder ordering, and DEFAULT variant byte-length assertion.
Mode field: TurnOptions, SendOptions, SpawnOptions, schemas
harness/src/types/turn.rs, harness/src/functions/send.rs, harness/src/functions/spawn.rs, harness/tests/golden/schemas/harness.run.json, harness/tests/golden/schemas/harness.send.json, harness/tests/golden/schemas/harness.spawn.json
Adds optional mode: Option<Mode> to TurnOptions, SendOptions, SpawnOptions; updates all three golden JSON schemas with Mode enum definition and optional mode property.
Console: remove system-prompt.ts, pass mode instead
console/web/src/lib/backend/system-prompt.ts, console/web/src/lib/backend/harness-send.ts, console/web/src/lib/backend/real.ts, console/web/README.md
Deletes system-prompt.ts (260 lines); adds HarnessSendMode type and mode field to HarnessSendOptions; real.ts removes buildModeSystemPrompt and passes mode directly to harness; README removes the deleted file from the directory listing.
send.rs and subagent.rs wired to resolve_system_prompt
harness/src/functions/send.rs, harness/src/subagent.rs
build_options calls prompt::resolve_system_prompt with per-send system_prompt, mode, and provider; subagent resolves system_prompt the same way using request mode and selected provider; tests validate built-in and override prompt paths.
Empty messages safety
context-manager/src/core/selection.rs, context-manager/tests/features/assemble.feature, context-manager/tests/steps/invariant_steps.rs, harness/src/turn_loop.rs, provider-anthropic/src/stream_fn.rs
select() guarantees a non-empty verbatim tail; run_step fails fast when gen_messages is empty; assemble_context falls back to raw candidate messages when assembled result is empty; provider-anthropic refuses upstream calls with empty messages; BDD scenario and step added.
Harness README and architecture docs
harness/README.md, harness/architecture/integration.md
README adds a System prompt section documenting identity prompt selection, mode prefix, and override semantics; integration.md updates options.mode/options.system_prompt guidance and the Console reference table.

Session messages pagination

Layer / File(s) Summary
SessionClient::messages pagination
harness/src/clients/session.rs
Replaces single-request messages fetch with a PAGE_LIMIT = 500 loop that follows next_cursor until absent, accumulating full transcript entries.

iii-sdk v0.19.4 binary-worker docs update

Layer / File(s) Summary
binary-worker.md updated for iii-sdk v0.19.4 API
docs/sops/binary-worker.md
Pins iii-sdk to =0.19.4 in Cargo.toml examples and skeleton; updates register_worker/InitOptions import shapes; updates RegisterFunction::new_async signature to receive only the closure, with function id passed to iii.register_function separately.

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

  • iii-hq/workers#151: Directly related — also updates console/web/src/lib/backend/real.ts to stop passing a computed system_prompt from the console and shift prompt construction to the harness.
  • iii-hq/workers#227: Directly related — implements per-provider/per-mode identity prompt construction inside the harness, the same system this PR generalizes into harness/src/prompt/.

Suggested reviewers

  • ytallo

Poem

🐇 Hoppity-hop, the prompts have moved!
No more console crafting what harness behooved.
Each provider gets its own little text,
Mode paragraphs prefix — what clever effects!
And empty arrays? We refuse them outright.
The rabbit checks messages before taking flight! ✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately captures the main changes: moving system prompt assembly from client-side TypeScript to the harness backend and improving message selection logic to prevent empty arrays.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/harness-system-prompt

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 and usage tips.

@github-actions

github-actions Bot commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

skill-check — worker

0 verified, 22 skipped (no docs/).

Layer Result
structure
vale
ai
render

Four for four. Nicely done.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
docs/sops/binary-worker.md (2)

694-731: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Assert description in the manifest test too.

Both manifest examples stop at name, version, default_config, and supported_targets. That lets a manifest with an empty or missing description pass even though section 5 says the registry requires it.

♻️ Add the missing check in both snippets
     assert_eq!(manifest["name"], env!("CARGO_PKG_NAME"));
     assert_eq!(manifest["version"], env!("CARGO_PKG_VERSION"));
+    assert!(
+        manifest["description"]
+            .as_str()
+            .map_or(false, |s| !s.is_empty()),
+        "description must not be empty"
+    );
     assert!(
         !manifest["default_config"].is_object(),
         "default_config must be an object"
     );

Also applies to: 1317-1344

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/sops/binary-worker.md` around lines 694 - 731, The
manifest_subcommand_emits_valid_json test is missing validation for the
description field, which the registry requires according to section 5. Add an
assertion after the existing manifest field checks to verify that the
description field exists and is not empty, similar to how name, version,
default_config, and supported_targets are validated. This same assertion needs
to be added to the other manifest test snippet referenced in the "Also applies
to" section (lines 1317-1344).

138-160: ⚠️ Potential issue | 🟠 Major

Confirm the SDK version bump and API shape before finalizing this scaffold.

The docs pin iii-sdk = "=0.19.4", but the locked versions across workers show 0.19.2 (harness, session-manager, provider-openai, provider-anthropic, llm-router, lsp, context-manager, approval-gate), 0.19.1-next.1 (shell, codex), 0.19.0 (storage), and 0.16.0-next.2 (database, coder, console, acp, email, mcp, iii-directory, image-resize). Additionally, the existing shell/src/main.rs still constructs InitOptions { otel: Some(OtelConfig::default()), ..Default::default() }, while the doc examples use InitOptions { metadata: Some(WorkerMetadata { ... }), ..InitOptions::default() }. If 0.19.4 and the new API shape are not yet released, this scaffold will diverge from the actual pinned SDK versions. Ensure the version and constructor signatures match before treating this as the single source of truth.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/sops/binary-worker.md` around lines 138 - 160, Verify the actual pinned
iii-sdk version across all workers in the repository (check Cargo.lock files in
harness, session-manager, provider-openai, provider-anthropic, llm-router, lsp,
context-manager, approval-gate, shell, codex, storage, database, coder, console,
acp, email, mcp, iii-directory, and image-resize) and confirm the current API
shape for the InitOptions constructor pattern used in existing workers like
shell/src/main.rs. Update the Cargo.toml pinned version and the InitOptions
constructor examples in the documentation to match the actual versions and API
signatures in use across the codebase, ensuring all documentation reflects the
true current state rather than a future state.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@context-manager/src/core/selection.rs`:
- Around line 136-144: The condition `if start > 0` on line 138 prevents the
valid case of `start == 0` from returning early, causing the code to fall
through and unnecessarily apply compaction. The fix is to remove the nested `if
start > 0` guard entirely so that the Selection struct with `head_len: start`
and `tail_start_index: Some(start)` is returned immediately whenever the outer
`if let Some(start) = keep` condition is true, allowing the case where start
equals 0 to preserve the full verbatim history without forced summarization.

In `@harness/architecture/integration.md`:
- Around line 275-277: Clarify the documentation for the system_prompt field to
explicitly state that both omitting the field and providing an empty string ("")
result in the same behavior: the built-in identity prompt will be used. Update
the comment or description to make callers aware that resolve_system_prompt
treats these two cases identically, preventing confusion when an empty string is
serialized and unexpectedly falls back to the default prompt. This applies to
the system_prompt field and any other similar fields mentioned in the related
sections.

In `@harness/prompts/anthropic.txt`:
- Around line 84-90: The rule statement "BEFORE you call ANY function, fetch its
contract" is too absolute and creates a logical contradiction with the
documented exceptions for discovery and registry calls mentioned later in the
prompt. Revise the opening of RULE 2 to clarify that the contract-fetch
requirement applies to regular function calls but explicitly excludes the
discovery/registry operations like engine::functions::list,
engine::triggers::list, and registry lookup calls. Make it clear that these
specific calls are exempt from the contract-fetching requirement, so the rule is
internally consistent.

In `@harness/prompts/gpt.txt`:
- Around line 53-59: The opening rule statement "BEFORE you call ANY function,
fetch its contract" is too broad and creates a logical conflict with the
documented exemptions for discovery/registry calls like engine::functions::list,
engine::triggers::list, and registry lookup calls. Revise the rule to explicitly
carve out these exemptions upfront, making it clear that contract fetching is
required for general function calls but not for the specified discovery and
registry calls. This should remove the apparent contradiction between the broad
opening statement and the later documented exceptions.

In `@harness/src/clients/session.rs`:
- Around line 206-233: The pagination loop in the messages method trusts the
next_cursor unconditionally, which could cause an infinite loop if the upstream
API returns the same non-empty cursor repeatedly. Add a guard mechanism to track
previously-seen cursors before the loop starts, then check if the new cursor
from next_cursor has already been visited before updating the cursor variable
and continuing the loop. If the cursor has been seen before, break out of the
loop to prevent infinite pagination. This ensures the loop makes progress or
exits rather than spinning on the same cursor value indefinitely.

In `@provider-anthropic/src/stream_fn.rs`:
- Around line 105-120: The empty messages validation check in the `if
input.messages.is_empty()` block is currently positioned after token loading and
configuration metadata work, which wastes resources for invalid inputs. Move
this entire validation block to execute immediately after line 54 in the
function stream_fn, before any token loading or router/config/model metadata
calls occur, so that empty message inputs are rejected early via the
synthetic_error_event without incurring unnecessary work.

---

Outside diff comments:
In `@docs/sops/binary-worker.md`:
- Around line 694-731: The manifest_subcommand_emits_valid_json test is missing
validation for the description field, which the registry requires according to
section 5. Add an assertion after the existing manifest field checks to verify
that the description field exists and is not empty, similar to how name,
version, default_config, and supported_targets are validated. This same
assertion needs to be added to the other manifest test snippet referenced in the
"Also applies to" section (lines 1317-1344).
- Around line 138-160: Verify the actual pinned iii-sdk version across all
workers in the repository (check Cargo.lock files in harness, session-manager,
provider-openai, provider-anthropic, llm-router, lsp, context-manager,
approval-gate, shell, codex, storage, database, coder, console, acp, email, mcp,
iii-directory, and image-resize) and confirm the current API shape for the
InitOptions constructor pattern used in existing workers like shell/src/main.rs.
Update the Cargo.toml pinned version and the InitOptions constructor examples in
the documentation to match the actual versions and API signatures in use across
the codebase, ensuring all documentation reflects the true current state rather
than a future state.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 8ee1e4ab-84ae-440e-ba4f-38618fabb235

📥 Commits

Reviewing files that changed from the base of the PR and between 9419c0a and 50c670d.

📒 Files selected for processing (31)
  • console/web/README.md
  • console/web/src/lib/backend/harness-send.ts
  • console/web/src/lib/backend/real.ts
  • console/web/src/lib/backend/system-prompt.ts
  • context-manager/src/core/selection.rs
  • context-manager/tests/features/assemble.feature
  • context-manager/tests/steps/invariant_steps.rs
  • docs/sops/binary-worker.md
  • harness/README.md
  • harness/architecture/integration.md
  • harness/prompts/anthropic.txt
  • harness/prompts/cli.txt
  • harness/prompts/default.txt
  • harness/prompts/gpt.txt
  • harness/prompts/kimi.txt
  • harness/src/clients/session.rs
  • harness/src/functions/send.rs
  • harness/src/functions/spawn.rs
  • harness/src/lib.rs
  • harness/src/prompt/family.rs
  • harness/src/prompt/mod.rs
  • harness/src/prompt/mode.rs
  • harness/src/prompt/tests.rs
  • harness/src/prompt/variants.rs
  • harness/src/subagent.rs
  • harness/src/turn_loop.rs
  • harness/src/types/turn.rs
  • harness/tests/golden/schemas/harness.run.json
  • harness/tests/golden/schemas/harness.send.json
  • harness/tests/golden/schemas/harness.spawn.json
  • provider-anthropic/src/stream_fn.rs
💤 Files with no reviewable changes (2)
  • console/web/src/lib/backend/system-prompt.ts
  • console/web/README.md

Comment thread context-manager/src/core/selection.rs Outdated
Comment thread harness/architecture/integration.md
Comment on lines +84 to +90
RULE 2 — BEFORE you call ANY function, fetch its contract from the engine by passing that
function's id as `function_id` to `engine::functions::info`. A one-line description from
`engine::functions::list` is a HINT, not the contract — `info` is the contract. Shape your
`payload` to match that schema EXACTLY: every required field, the right value formats (single
binary vs argv array, inline string vs base64, "K=V" entries), and NO field the schema does not
define. Guessing or remembering field names burns turns on retries and can put workers into
degraded states. A contract you already fetched this turn does not need refetching.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Clarify the contract-fetch rule for documented discovery/registry calls.

BEFORE you call ANY function conflicts with the later exceptions for engine::functions::list, engine::triggers::list, and the registry lookup calls in this same prompt. As written, the model is told to fetch a contract for calls you explicitly exempt below.

Suggested rewrite
- RULE 2 — BEFORE you call ANY function, fetch its contract from the engine by passing that
- function's id as `function_id` to `engine::functions::info`.
+ RULE 2 — BEFORE you call any non-discovery function, fetch its contract from the engine by
+ passing that function's id as `function_id` to `engine::functions::info`.

Also applies to: 135-149

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@harness/prompts/anthropic.txt` around lines 84 - 90, The rule statement
"BEFORE you call ANY function, fetch its contract" is too absolute and creates a
logical contradiction with the documented exceptions for discovery and registry
calls mentioned later in the prompt. Revise the opening of RULE 2 to clarify
that the contract-fetch requirement applies to regular function calls but
explicitly excludes the discovery/registry operations like
engine::functions::list, engine::triggers::list, and registry lookup calls. Make
it clear that these specific calls are exempt from the contract-fetching
requirement, so the rule is internally consistent.

Comment thread harness/prompts/gpt.txt
Comment on lines +53 to +59
Two rules govern every call. BEFORE you call ANY function, fetch its contract by passing its id
as `function_id` to `engine::functions::info` — a one-line `list` description is a hint,
not the contract. Then shape the payload to that schema exactly: every required field, the
right value formats (single binary vs argv array, inline string vs base64, "K=V" entries), no
field the schema does not define. Guessing field names burns turns on retries and can put
workers into degraded states. A contract you already fetched this turn does not need
refetching.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Clarify the contract-fetch rule for documented discovery/registry calls.

BEFORE you call ANY function conflicts with the later carve-outs for engine::functions::list, engine::triggers::list, and the registry lookup calls. The prompt should not require a contract fetch for calls it explicitly says are exempt.

Suggested rewrite
- Two rules govern every call. BEFORE you call ANY function, fetch its contract by passing its id
- as `function_id` to `engine::functions::info` — a one-line `list` description is a hint,
- not the contract.
+ Two rules govern every non-discovery call. BEFORE you call it, fetch its contract by passing
+ its id as `function_id` to `engine::functions::info` — a one-line `list` description is a hint,
+ not the contract.

Also applies to: 118-130

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@harness/prompts/gpt.txt` around lines 53 - 59, The opening rule statement
"BEFORE you call ANY function, fetch its contract" is too broad and creates a
logical conflict with the documented exemptions for discovery/registry calls
like engine::functions::list, engine::triggers::list, and registry lookup calls.
Revise the rule to explicitly carve out these exemptions upfront, making it
clear that contract fetching is required for general function calls but not for
the specified discovery and registry calls. This should remove the apparent
contradiction between the broad opening statement and the later documented
exceptions.

Comment on lines +206 to +233
let mut cursor: Option<String> = None;
loop {
let mut payload = json!({
"session_id": session_id,
"include_custom": include_custom,
"limit": PAGE_LIMIT,
});
if let Some(c) = &cursor {
payload["cursor"] = json!(c);
}
let resp = self.call("session::messages", payload).await?;
let arr = resp
.get("messages")
.and_then(Value::as_array)
.cloned()
.unwrap_or_default();
for item in arr {
match serde_json::from_value::<LoadedEntry>(item) {
Ok(entry) => out.push(entry),
Err(e) => {
tracing::warn!(session_id, error = %e, "skipping unparseable session entry")
}
}
}
match resp.get("next_cursor").and_then(Value::as_str) {
Some(next) if !next.is_empty() => cursor = Some(next.to_string()),
_ => break,
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Add a non-advancing cursor guard to prevent infinite pagination loops.

At Line 230–233, the loop trusts next_cursor unconditionally. If upstream ever returns the same non-empty cursor repeatedly, messages() can spin forever and stall turn execution. Add a progress check (or visited-cursor set) before continuing.

Proposed fix
     pub async fn messages(
         &self,
         session_id: &str,
         include_custom: bool,
     ) -> Result<Vec<LoadedEntry>, HarnessError> {
         const PAGE_LIMIT: u64 = 500;
         let mut out: Vec<LoadedEntry> = Vec::new();
         let mut cursor: Option<String> = None;
+        let mut last_cursor: Option<String> = None;
         loop {
             let mut payload = json!({
                 "session_id": session_id,
                 "include_custom": include_custom,
                 "limit": PAGE_LIMIT,
             });
             if let Some(c) = &cursor {
                 payload["cursor"] = json!(c);
             }
             let resp = self.call("session::messages", payload).await?;
@@
             }
             match resp.get("next_cursor").and_then(Value::as_str) {
-                Some(next) if !next.is_empty() => cursor = Some(next.to_string()),
+                Some(next) if !next.is_empty() => {
+                    if last_cursor.as_deref() == Some(next) {
+                        tracing::warn!(
+                            session_id,
+                            cursor = next,
+                            "session::messages returned a non-advancing cursor; stopping pagination"
+                        );
+                        break;
+                    }
+                    last_cursor = Some(next.to_string());
+                    cursor = Some(next.to_string());
+                }
                 _ => break,
             }
         }
         Ok(out)
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let mut cursor: Option<String> = None;
loop {
let mut payload = json!({
"session_id": session_id,
"include_custom": include_custom,
"limit": PAGE_LIMIT,
});
if let Some(c) = &cursor {
payload["cursor"] = json!(c);
}
let resp = self.call("session::messages", payload).await?;
let arr = resp
.get("messages")
.and_then(Value::as_array)
.cloned()
.unwrap_or_default();
for item in arr {
match serde_json::from_value::<LoadedEntry>(item) {
Ok(entry) => out.push(entry),
Err(e) => {
tracing::warn!(session_id, error = %e, "skipping unparseable session entry")
}
}
}
match resp.get("next_cursor").and_then(Value::as_str) {
Some(next) if !next.is_empty() => cursor = Some(next.to_string()),
_ => break,
}
let mut cursor: Option<String> = None;
let mut last_cursor: Option<String> = None;
loop {
let mut payload = json!({
"session_id": session_id,
"include_custom": include_custom,
"limit": PAGE_LIMIT,
});
if let Some(c) = &cursor {
payload["cursor"] = json!(c);
}
let resp = self.call("session::messages", payload).await?;
let arr = resp
.get("messages")
.and_then(Value::as_array)
.cloned()
.unwrap_or_default();
for item in arr {
match serde_json::from_value::<LoadedEntry>(item) {
Ok(entry) => out.push(entry),
Err(e) => {
tracing::warn!(session_id, error = %e, "skipping unparseable session entry")
}
}
}
match resp.get("next_cursor").and_then(Value::as_str) {
Some(next) if !next.is_empty() => {
if last_cursor.as_deref() == Some(next) {
tracing::warn!(
session_id,
cursor = next,
"session::messages returned a non-advancing cursor; stopping pagination"
);
break;
}
last_cursor = Some(next.to_string());
cursor = Some(next.to_string());
}
_ => break,
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@harness/src/clients/session.rs` around lines 206 - 233, The pagination loop
in the messages method trusts the next_cursor unconditionally, which could cause
an infinite loop if the upstream API returns the same non-empty cursor
repeatedly. Add a guard mechanism to track previously-seen cursors before the
loop starts, then check if the new cursor from next_cursor has already been
visited before updating the cursor variable and continuing the loop. If the
cursor has been seen before, break out of the loop to prevent infinite
pagination. This ensures the loop makes progress or exits rather than spinning
on the same cursor value indefinitely.

Comment on lines +105 to +120
// Defense in depth: never POST an empty messages array — Anthropic rejects
// it with a 400 ("messages: at least one message is required"). Surface a
// clear provider error frame instead of a cryptic upstream failure. The
// harness/context-manager guards make this unreachable in practice.
if input.messages.is_empty() {
let _ = send_event(
sink,
&synthetic_error_event(
"refusing to call anthropic with an empty messages array \
(messages: at least one message is required)",
&model,
ErrorKind::Permanent,
),
);
return;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Move empty-message validation before resolve/config work.

Right now (Line 109) the guard runs after token load + router/config/model metadata calls. For empty input, that can produce unrelated errors first and adds avoidable dependency work. Short-circuit this check immediately after Line 54.

Suggested fix
 async fn run_stream_call(
@@
 ) {
     let model = input.model.clone();
+
+    if input.messages.is_empty() {
+        let _ = send_event(
+            sink,
+            &synthetic_error_event(
+                "refusing to call anthropic with an empty messages array \
+                 (messages: at least one message is required)",
+                &model,
+                ErrorKind::Permanent,
+            ),
+        );
+        return;
+    }
 
     let mut warnings = Vec::new();
@@
-    if input.messages.is_empty() {
-        let _ = send_event(
-            sink,
-            &synthetic_error_event(
-                "refusing to call anthropic with an empty messages array \
-                 (messages: at least one message is required)",
-                &model,
-                ErrorKind::Permanent,
-            ),
-        );
-        return;
-    }
-
     let body = build_body(&BodyArgs {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@provider-anthropic/src/stream_fn.rs` around lines 105 - 120, The empty
messages validation check in the `if input.messages.is_empty()` block is
currently positioned after token loading and configuration metadata work, which
wastes resources for invalid inputs. Move this entire validation block to
execute immediately after line 54 in the function stream_fn, before any token
loading or router/config/model metadata calls occur, so that empty message
inputs are rejected early via the synthetic_error_event without incurring
unnecessary work.

…election logic

- Removed the obsolete `system-prompt.ts` file, consolidating system prompt assembly directly within the harness.
- Introduced a new `HarnessSendMode` type to manage operating modes (`plan`, `ask`, `agent`) for message handling.
- Updated the `real.ts` file to utilize the new mode handling, ensuring the harness constructs identity prompts dynamically based on the provided mode.
- Enhanced the message selection logic in `selection.rs` to prevent empty message arrays when summarizing history, ensuring at least the last turn is retained.
- Added tests to verify the new behavior of message selection and system prompt handling, ensuring compliance with the updated logic.
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.

3 participants