Skip to content

proposal: make opencode harness calls explicit #960

Closed

Description

@ddbaron

Proposal: make OpenCode harness calls explicit

Summary

I’d like to propose updating the AgentField OpenCode provider to build a
per-run OPENCODE_CONFIG_CONTENT overlay from the existing harness(...)
options. Each opencode run would select a fixed agent with --agent, attach
the system prompt and model to that agent, use steps: 500, apply the supplied
reasoning variant, and run with explicit noninteractive permissions.

This would let OpenCode receive the same intent that the caller already
expressed through the harness API, without changing SWE-AF-specific workflows.
SWE-AF is one consumer of this behavior, but the change should stay generic and
live in the AgentField OpenCode provider.

Problem

The harness already accepts system_prompt, tools, model, variant, cwd,
max_turns, and permission_mode. The OpenCode provider currently uses only a
subset of those values when it builds a command similar to:

opencode run --format json --dir <CWD> -m <MODEL> <PROMPT>

The system prompt is folded into the positional prompt as ordinary text,
tools is not translated into OpenCode permissions, and
permission_mode does not establish a reliable noninteractive boundary. As a
result, a run can depend on ambient OpenCode defaults, use an unintended agent,
duplicate system instructions, or wait for a permission approval that nobody
can provide.

Proposed change

For each standalone OpenCode subprocess, the provider would generate an
OPENCODE_CONFIG_CONTENT overlay with one fixed generic agent name, such as
agentfield-harness. The deployment can continue to own the shared settings
for plugins, providers, model limits, and compaction. Meaning: if you customize your opencode deployment already, this will pickup your global mcp, plugins, etc... and only superceed what is declared.

The generated agent should have this shape:

{
  "$schema": "https://opencode.ai/config.json",
  "default_agent": "agentfield-harness",
  "agent": {
    "agentfield-harness": {
      "prompt": "<SYSTEM_PROMPT>",
      "model": "<MODEL>",
      "mode": "primary",
      "steps": 500,
      "reasoningEffort": "<VARIANT>",
      "permission": {
        "*": "allow",
        "question": "deny",
        "task": "deny"
      }
    }
  }
}

reasoningEffort would be omitted when no reasoning variant is supplied. The
AgentField variant and OpenCode reasoningEffort values represent one
logical reasoning-effort setting. When it is supplied, the provider would use
the same value for the agent's reasoningEffort and the OpenCode --variant
command option rather than treating them as independent settings.

At the process boundary, the command would look like this:

OPENCODE_CONFIG_CONTENT=<GENERATED_CONFIG> opencode run --format json --agent agentfield-harness --dir <CWD> -m <MODEL> [--variant <VARIANT>] <PROMPT>

The provider would set OPENCODE_CONFIG_CONTENT in the child process
environment. The SWE-AF caller would continue to pass normal harness options;
it would not need to construct this shell command.

The provider should pass only the task prompt positionally. It should not add a
SYSTEM INSTRUCTIONS: section because the system prompt is now attached to the
OpenCode agent.

Turn and permission semantics

One important distinction is that max_turns must not be mapped to OpenCode
steps. They are different budgets: steps is OpenCode's internal agent
iteration limit and should remain a large, fixed value of 500, while
max_turns remains an AgentField/SWE-AF loop setting.

Map recognized AgentField tools to OpenCode actions as follows:

Read  -> read
Write -> edit
Edit  -> edit
Glob  -> glob
Grep  -> grep
Bash  -> bash

For the initial integration, I suggest an allow baseline rather than turning
every omitted tool into a deny rule. Explicitly deny only question and task,
so an autonomous run never waits for an approval that cannot be supplied. This
is a compatibility baseline, not yet strict per-role authorization.

That choice is driven by AgentField's current structured-output protocol:
schema-backed runs ask the agent to write .agentfield_output.json, and
OpenCode maps both AgentField Write and Edit to its edit permission. A
strict edit denial would therefore break structured output. Path-limited output
permissions or a different output transport can be considered separately.

Why I think this is useful

  • The system prompt becomes an actual OpenCode agent prompt instead of being
    repeated in the user-facing request.
  • --agent and default_agent make the selected agent deterministic.
  • The model remains available as an agent default while -m explicitly selects
    the model for a particular run.
  • Reasoning effort is represented consistently in the agent configuration and by
    the per-run variant option.
  • Headless runs can proceed autonomously instead of blocking on permission
    prompts.
  • Per-run configuration lives in the subprocess environment, so concurrent
    runs do not need to share and mutate a temporary config file.

Strategy and scope

I’d keep the first change deliberately narrow:

  1. Add the OpenCode config-overlay construction to the AgentField OpenCode
    provider.
  2. Preserve the existing model and directory options, adding --agent and an
    optional --variant.
  3. Make variant an explicit harness option on SDK surfaces where it is not
    already typed, while preserving existing model-variant behavior.
  4. Keep max_turns outside the OpenCode configuration.
  5. Add provider tests for the generated configuration, command construction,
    prompt handling, permission defaults, fixed steps: 500, and variant
    handling. Maintain Python, Go, and TypeScript parity where those providers
    expose the same harness contract.

I would leave opencode serve/--attach, a role/profile registry, per-role MCP
authorization, and a replacement structured-output transport out of the first
patch. Attached sessions load configuration on the server and would need a
separate per-session configuration design.

Acceptance criteria

  • A standalone OpenCode run receives the generated OPENCODE_CONFIG_CONTENT.
  • The selected agent, system prompt, model, and permissions are visible in the
    effective OpenCode configuration.
  • steps is always 500 in this overlay; max_turns is never serialized as
    steps.
  • A supplied reasoning variant is represented as reasoningEffort in the
    agent configuration and as --variant on the command line.
  • question and task are denied, with no generated ask permissions.
  • The positional prompt contains the task only, not a second copy of the
    system prompt.
  • Existing caller/deployment environment and provider behavior remain
    compatible when no OpenCode-specific overrides are supplied.

Feedback requested

Before implementing this, I’d appreciate confirmation that
OPENCODE_CONFIG_CONTENT is the right extension point for per-run OpenCode
configuration, that the fixed agentfield-harness name is acceptable, and that
Python, Go, and TypeScript provider parity is expected for this change. If this
direction looks right, I plan to prepare it as a focused provider-and-tests PR.

note: gpt luna helped me cleanup the proposal and make it (hopefully) more presentable.

Activity

  1. AbirAbbas commented on Aug 25, 2026

    @AbirAbbas
    Contributor

    Triage

    Classification: feature ✓
    Suggested labels: enhancement

    Likely affected files:

    • ./reasoners/models.py (matched: model, one, model, model, one, model, model, model, mode, one, model, one, model, model, model, model)
    • ./reasoners/repo_config.py (matched: one, one, one, one, config)
    • ./reasoners/webhook_router.py (matched: one, one, one, one, out)
    • ./reasoners/init.py (matched: one, one, one, one)
    • ./reasoners/conversational.py (matched: one, one, one, one)

    Triage by github-buddy

  2. AbirAbbas commented on Aug 26, 2026

    @AbirAbbas
    Contributor

    Thanks for writing this up properly and asking before building it out — and sorry you closed #961 before we got to it.

    I checked the proposal against opencode 1.14.33 rather than guessing, and the mechanism holds up: --agent is a real flag on run, and every key in your overlay (default_agent, mode, steps, reasoningEffort, permission.question, permission.task) is accepted by opencode's strict validator (an unrecognized key hard-fails with Configuration is invalid at OPENCODE_CONFIG_CONTENT, so passing means recognized). Your claim that a deployment keeps owning mcp/plugins is correct for file-based config — with a deliberately bogus key in $XDG_CONFIG_HOME/opencode/opencode.json and OPENCODE_CONFIG_CONTENT set, opencode still errored on the file, so it's still loaded and merged.

    So: yes to the extension point, yes to agentfield-harness as the fixed name, yes to denying question/task, and yes to not double-shipping the system prompt. steps: 500 is fine as a value — make it a named constant with an env override rather than a bare literal.

    Three things need to change from #961 before this can land:

    1. The overlay must merge, not clobber. In feat(sdk/python): configure OpenCode harness per run #961 env["OPENCODE_CONFIG_CONTENT"] = ... is assigned unconditionally after the caller env is copied in; a caller-supplied value is destroyed. Compare sdk/go/harness/opencode.go:139 and sdk/typescript/src/harness/providers/opencode.ts:81-82, where the same env var already carries the OpenRouter attribution overlay behind a "caller didn't set it" guard. Deep-merge into the caller/ambient value instead, so attribution and the harness agent can coexist.

    2. Silent system-prompt loss needs an answer. opencode run --agent no-such-agent doesn't fail — it prints a warning, falls back to the default agent, and exits 0. If the overlay ever fails to reach the child (env stripped by a wrapper or container) we'd run with no system prompt at all and still report success. SWE-AF and pr-af push long system prompts through this path, so I want an opt-out (env or a harness option) that restores the inline path for one release, so they can A/B and roll back without pinning an old SDK. Related: moving a large system prompt from stdin/argv into an env var has its own size limits — worth a test.

    3. Drop the tool→permission mapping for now. With "*": "allow" present and HarnessConfig.tools defaulting to all six, the mapping only ever writes allow on top of allow. Leave it out until there's a strict mode that actually denies. Same for permission_mode: feat(sdk/python): configure OpenCode harness per run #961 still ignores it, and the docs should say so rather than imply it's wired.

    On parity: yes, py/go/ts should agree eventually, but not in one PR. Land Python first with the merge fix and the opt-out, smoke it against a real model with an SWE-AF-shaped prompt, then port — the Go/TS ports have to be co-designed with the attribution overlay.

    Two notes on #961 specifically: the variant addition to HarnessConfig/HarnessRunner.run is good and provider-agnostic — split that out and it can go in on its own. And please drop the ruff format churn in agent.py/types.py; CI doesn't gate formatting and it collides with #913 (Pi/OMP harnesses), which is conflicting right now and touches _runner.py, types.py and the harness docs. Suggest we get #913 sorted, then you reopen on top of it. Separately, #947 is closing out your #813.

  3. ddbaron commented on Aug 27, 2026

    @ddbaron
    ContributorAuthor

    Thank you for the replies and direction. I’m working on the PR now.

    During testing, I found a recursion issue. AgentField-launched OpenCode sessions must not invoke AgentField orchestration themselves. In other words, OpenCode instances launched by AgentField should not use af.

    This appears to be happening because the AgentField skills are registered globally and therefore become available inside those OpenCode sessions.

    I’ll address this in the PR. I think the best approach is to preserve allowing desirable global OpenCode configuration, such as MCP servers and plugins, while applying a custom configuration to AgentField-launched OpenCode instances that explicitly denies access to the agentfield* skills.

  4. ddbaron commented on Aug 31, 2026

    @ddbaron
    ContributorAuthor

    The proposal is now split into two independently reviewable upstream pull requests:

    The variant change is independently applicable to main; the two pull requests are related but not codependent.

  5. AbirAbbas commented on Aug 31, 2026

    @AbirAbbas
    Contributor

    Good catch on the recursion, and your diagnosis is the right one: the trigger is the globally registered skills becoming visible inside the OpenCode session, so the config-level deny of the agentfield* skills is the correct enforcement layer — please include it in the PR.

    What shipped on our side in v0.1.137 (#1003): every harness subprocess gets AGENTFIELD_HARNESS_DEPTH (set and incremented by the Python, Go and TypeScript harness CLIs), and both agentfield skills tell the model not to dispatch when it's set. That's advisory — it stops a cooperating model, not a determined one — which is why your deny matters. I looked at also stripping AGENTFIELD_SERVER / the API key from the child env and it wouldn't help this case: every consumer hard-defaults to localhost:8080 when the variable is absent, so a nested session would still find the control plane.

    The three review conditions from before still stand: merge the OPENCODE_CONFIG_CONTENT overlay into any caller/ambient value (don't clobber), keep an opt-out that restores the inline system-prompt path for one release, and skip the tool→permission map until there's a strict mode.

    @santoshkumarradha one policy call: deny nested harness orchestration by default (enforced, not advisory)? My take is not this cycle — keep it advisory, let this PR's skill deny be the enforcement for OpenCode, and if we do flip a default later, deny at depth ≥ 2 so a single level of tool use still works.

  6. ddbaron commented on Aug 31, 2026

    @ddbaron
    ContributorAuthor

    @AbirAbbas thanks, I believe I've met the three review conditions via the two PRs #1023 and #1024, and I just rebased each PR on latest available main.

  7. AbirAbbas commented on Sep 21, 2026

    @AbirAbbas
    Contributor

    The Go and TypeScript halves are up in #1064, under the same three conditions #1023 landed on: the overlay deep-merges into any caller/ambient OPENCODE_CONFIG_CONTENT (so a deployment's mcp/plugins/agents and the OpenRouter attribution overlay survive), AGENTFIELD_OPENCODE_INLINE_SYSTEM_PROMPT keeps the inline path available for a release, and tools/permission_mode stay untranslated. steps became a named constant with an AGENTFIELD_OPENCODE_STEPS override in all three SDKs.

    Validated by dispatching a real reasoner through a live control plane and checking what the opencode child actually received — --agent agentfield-harness, the task as the only positional prompt, the caller's mcp block and extra agent still present in the merged config — then replaying that exact config against opencode 1.14.33: it passes the strict validator and resolves the agent, where the same command without the overlay falls back to the default agent.

    One thing worth flagging from the review: Go was getting the permission ordering right only by accident (encoding/json sorts map keys), so a caller rule like skill: {"agentfield-*": "allow"} would have serialized after agentfield*: deny and won the last-match. That's fixed with explicit ordering. Also, the TypeScript provider was reading options.system_prompt while HarnessRunner passes systemPrompt, so a system prompt set through the public TS API never reached opencode at all — it now accepts both.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions