Skip to content

feat(agentx): add orchestrator detection with X-Orchestrator header#60

Merged
rsnodgrass merged 4 commits into
mainfrom
ryan/pty-mode-research
Feb 24, 2026
Merged

feat(agentx): add orchestrator detection with X-Orchestrator header#60
rsnodgrass merged 4 commits into
mainfrom
ryan/pty-mode-research

Conversation

@rsnodgrass
Copy link
Copy Markdown
Contributor

@rsnodgrass rsnodgrass commented Feb 24, 2026

Summary

Implement first-class orchestrator support in agentx to distinguish platforms that launch coding agents (OpenClaw, Conductor) from the coding agents themselves. Orchestrators are now reported via the X-Orchestrator HTTP header on all SageOx API requests.

Key changes:

  • Added AgentRole type system (RoleAgent/RoleOrchestrator) to distinguish agent types
  • Implement DetectOrchestrator() in registry + two orchestrator implementations (Conductor, OpenClaw)
  • ORCHESTRATOR_ENV env var for explicit orchestrator identification (separate from AGENT_ENV)
  • X-Orchestrator header sent on all SageOx API requests via useragent.SetHeaders()
  • 23 SageOx API call sites updated to use the new header helper
  • 3 registry-level tests for DetectOrchestrator()

Test Plan

  • All 5027 existing tests pass
  • New tests: TestDetectOrchestrator, TestDetectOrchestrator_NoneRegistered, TestDetect_SkipsOrchestrators
  • New tests: TestSetHeaders_WithOrchestrator, TestSetHeaders_WithoutOrchestrator
  • Verify X-Orchestrator header is sent on all SageOx API requests (auth, api, lfs, telemetry, friction, etc.)
  • External git host calls (GitHub, GitLab) do NOT include the header (correct behavior)

Co-Authored-By: SageOx ox@sageox.ai

Summary by CodeRabbit

  • New Features

    • Added OpenClaw and Conductor orchestrators and explicit agent/orchestrator roles with environment-based detection and identity reporting.
  • Improvements

    • Unified User-Agent and orchestrator metadata handling across clients and propagated detected orchestrator into initialization/runtime metadata.
  • Tests

    • Added coverage for orchestrator detection, user-agent/header behavior, and related identity scenarios.

rsnodgrass and others added 2 commits February 23, 2026 20:49
Orchestrator context is already conveyed via X-Orchestrator header on all SageOx API requests. The header-level telemetry is sufficient; duplicating it in every event payload is redundant.

Co-Authored-By: SageOx <ox@sageox.ai>
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Feb 24, 2026

Warning

Rate limit exceeded

@rsnodgrass has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 1 minutes and 10 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between f37376a and 2d92eb0.

📒 Files selected for processing (14)
  • cmd/ox/doctor_ecosystem.go
  • internal/api/ledger.go
  • internal/api/repo.go
  • internal/api/repos.go
  • internal/auth/client.go
  • internal/auth/device_flow.go
  • internal/auth/refresh.go
  • internal/doctorapi/client.go
  • internal/lfs/client.go
  • internal/lfs/transfer.go
  • internal/session/summarize.go
  • internal/telemetry/client.go
  • internal/useragent/useragent.go
  • internal/uxfriction/client.go
📝 Walkthrough

Walkthrough

This PR centralizes User-Agent header construction, adds orchestrator detection and propagation into the user-agent context, introduces role-based agent distinctions, and registers two new orchestrators (OpenClaw, Conductor). It also adds orchestrator-aware header utilities and updates agent detection APIs and many agent implementations/tests.

Changes

Cohort / File(s) Summary
User-Agent Centralization
cmd/ox/doctor_ecosystem.go, internal/api/ledger.go, internal/api/repo.go, internal/api/repos.go, internal/auth/client.go, internal/auth/device_flow.go, internal/doctorapi/client.go, internal/lfs/client.go, internal/lfs/transfer.go, internal/session/summarize.go, internal/telemetry/client.go, internal/uxfriction/client.go
Replaced ad-hoc User-Agent header sets with useragent.SetHeaders() across HTTP request paths.
User-Agent Infrastructure
internal/useragent/useragent.go, internal/useragent/useragent_test.go
Added orchestrator state and APIs: SetOrchestratorType, OrchestratorType, SetHeaders; reworked UA string construction to token-based composition and added tests covering orchestrator behavior and header propagation.
Agent Role System & Orchestrator Types
pkg/agentx/agent.go, pkg/agentx/agent_test.go
Introduced AgentRole and RoleAgent/RoleOrchestrator, added AgentTypeOpenClaw/AgentTypeConductor, extended AgentIdentity/Detector interfaces for roles and orchestrator detection; tests updated.
Role Method on Coding Agents
pkg/agentx/agents/* (multiple files: aider.go, amp.go, claudecode.go, cline.go, codepuppy.go, cody.go, continue.go, copilot.go, cursor.go, droid.go, goose.go, kiro.go, opencode.go, windsurf.go)
Added Role() AgentRole implementations (returning RoleAgent) to existing coding agents.
Orchestrator Implementations & Tests
pkg/agentx/orchestrators/openclaw.go, pkg/agentx/orchestrators/openclaw_test.go, pkg/agentx/orchestrators/conductor.go, pkg/agentx/orchestrators/conductor_test.go, pkg/agentx/orchestrators/version_helpers.go
Added OpenClawAgent and ConductorAgent implementations with environment-based detection, identity methods, detection/version helpers, and unit tests.
Agent Registry & Setup
pkg/agentx/registry.go, pkg/agentx/setup/setup.go
Added role-aware detection helpers (detectByRole, DetectOrchestrator, DetectAll), orchestrator query helpers (CurrentOrchestrator, OrchestratorType), and registered new orchestrators in setup.
Auth Token Refresh Refactor
internal/auth/refresh.go
Refactored refresh code to client-scoped methods, renamed local token vars, and added EnsureValidToken / Handle401Error / receiver-based refreshToken methods.
Initialization Hook
cmd/ox/agent_prime.go
During agent prime initialization, call SetOrchestratorType with detected orchestrator type to propagate into user-agent context.

Sequence Diagram

sequenceDiagram
    participant Prime as Agent Prime
    participant Registry as Agent Registry
    participant Orchestrator as Orchestrator Agent
    participant UserAgent as User-Agent Module
    participant Client as HTTP Client

    Prime->>Registry: DetectOrchestrator(ctx)
    Registry->>Orchestrator: Detect(ctx, env)
    Orchestrator-->>Registry: Detected (type)
    Registry-->>Prime: Orchestrator Agent

    Prime->>UserAgent: SetOrchestratorType(agentType)
    UserAgent->>UserAgent: store orchestrator type, invalidate cache

    Client->>UserAgent: SetHeaders(req.Header)
    UserAgent->>UserAgent: compose UA tokens (include orchestrator if set)
    UserAgent->>Client: set `User-Agent` and `X-Orchestrator` headers
    Client->>Client: send HTTP request with headers
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰 I sniffed the env and found the clan,
OpenClaw and Conductor join the land.
Headers hop through one small gate,
Roles assigned — orchestrators wait.
Hooray, I dbg-nibble the change with a carrot stand! 🥕

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 47.17% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main feature: adding orchestrator detection and X-Orchestrator header support, which are central to this PR's objectives.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch ryan/pty-mode-research

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.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (5)
internal/useragent/useragent.go (1)

58-71: Unnecessary cache invalidation in SetOrchestratorType.

Line 70 clears cached, but cached is only used by String() which does not include orchestratorType in the User-Agent string. The invalidation is harmless but misleading — a future reader might assume orchestratorType participates in the cached UA string.

💡 Remove the unnecessary cache clear
 func SetOrchestratorType(ot string) {
 	if ot == "" {
 		return
 	}
 	mu.Lock()
 	defer mu.Unlock()
 	if orchestratorType != "" {
 		return
 	}
 	orchestratorType = ot
-	cached = ""
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/useragent/useragent.go` around lines 58 - 71, SetOrchestratorType
unnecessarily clears the package-level cached string even though cached is only
used by String() and String() does not include orchestratorType; remove the
misleading cache invalidation by deleting the line that sets cached = "" in
SetOrchestratorType (leave the rest of the function, including mu locking,
orchestratorType assignment, and the early returns, intact) so that cached is
not touched when setting orchestratorType.
pkg/agentx/orchestrators/conductor.go (2)

31-45: Same hardcoded string literal as OpenClaw — use the constant.

Line 32 uses "conductor" instead of string(agentx.AgentTypeConductor).

♻️ Use the constant
 func (a *ConductorAgent) Detect(_ context.Context, env agentx.Environment) (bool, error) {
-	if env.GetEnv("ORCHESTRATOR_ENV") == "conductor" {
+	if env.GetEnv("ORCHESTRATOR_ENV") == string(agentx.AgentTypeConductor) {
 		return true, nil
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/agentx/orchestrators/conductor.go` around lines 31 - 45, The Detect
method in ConductorAgent uses the hardcoded string "conductor" — replace that
literal with the canonical constant by calling string(agentx.AgentTypeConductor)
in the first env check (inside ConductorAgent.Detect) so the comparison uses the
shared AgentTypeConductor constant rather than a duplicated string; leave the
other env checks unchanged.

59-61: Capabilities stub returns empty — consider a TODO.

All capabilities are false. If Conductor supports any of these features now or soon, this should be updated. A brief TODO comment would help track this.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/agentx/orchestrators/conductor.go` around lines 59 - 61, The Capabilities
method on ConductorAgent currently returns an empty agentx.Capabilities struct;
update ConductorAgent.Capabilities to reflect any supported features (or at
minimum add a clear TODO comment) so this isn't silently all-false—locate the
Capabilities method on type ConductorAgent and either populate the returned
agentx.Capabilities fields that Conductor supports or add a TODO comment above
the function (e.g., "// TODO: populate supported capabilities such as X, Y")
explaining what needs to be enabled and when.
pkg/agentx/registry.go (1)

93-120: Clean refactor to detectByRole — non-deterministic winner when multiple match.

The refactoring from inline detection to detectByRole is well-structured. One subtlety: registry.List() iterates a map[AgentType]Agent, so if multiple orchestrators (or agents) match, the winner is non-deterministic across runs. With only two orchestrators today this is unlikely to matter, but if it ever does, consider sorting List() output or adding a priority field.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/agentx/registry.go` around lines 93 - 120, detectByRole currently
iterates registry.List() (which comes from a map) and can pick a
non-deterministic winner when multiple agents match; make the selection
deterministic by sorting the agents before iterating in detectByRole: obtain the
slice from registry.List(), sort it by a stable key (e.g., implement/use an
Agent.Priority() integer or fall back to Agent.Type() or name string) with a
stable comparison, then iterate the sorted slice to call agent.Detect(ctx, env);
update detectByRole to perform this sort so Agent selection is reproducible.
pkg/agentx/orchestrators/openclaw.go (1)

32-46: Use agentx.AgentTypeOpenClaw constant instead of hardcoded "openclaw" string.

Line 33 uses a hardcoded string literal instead of the defined constant. Since AgentType is a string type alias, using string(agentx.AgentTypeOpenClaw) ensures the comparison stays synchronized with the constant definition. The same pattern applies to Conductor at line 32 in conductor.go.

♻️ Use the constant
 func (a *OpenClawAgent) Detect(_ context.Context, env agentx.Environment) (bool, error) {
-	if env.GetEnv("ORCHESTRATOR_ENV") == "openclaw" {
+	if env.GetEnv("ORCHESTRATOR_ENV") == string(agentx.AgentTypeOpenClaw) {
 		return true, nil
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/agentx/orchestrators/openclaw.go` around lines 32 - 46, Replace the
hardcoded "openclaw" string in Detect with the AgentType constant: change the
comparison in OpenClawAgent.Detect to use string(agentx.AgentTypeOpenClaw)
instead of "openclaw"; also search for the same literal in conductor.go and
replace it with string(agentx.AgentTypeOpenClaw) to keep comparisons
synchronized with the AgentTypeOpenClaw constant.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@cmd/ox/doctor_ecosystem.go`:
- Around line 91-93: The request to GitHub must not include the X-Orchestrator
header; replace the call to useragent.SetHeaders(req.Header) in the GitHub API
request path with setting only the User-Agent header (e.g., read the user-agent
value via your useragent helper and set req.Header.Set("User-Agent", ...)) so
X-Orchestrator is not sent; update the code that currently calls
useragent.SetHeaders to instead set only the User-Agent header for the req used
in the GitHub call.

In `@internal/lfs/client.go`:
- Around line 125-128: The LFS batch request must not send the X-Orchestrator
header; remove the call to useragent.SetHeaders(req.Header) and instead only set
the User-Agent header on req (e.g., req.Header.Set("User-Agent", <useragent
string>)) so X-Orchestrator is not injected; locate the call to
useragent.SetHeaders in client.go alongside req.Header and c.authHeader and
replace it with code that obtains the user-agent string from the useragent
package (use its function that returns the UA string) and sets only the
"User-Agent" header.

In `@pkg/agentx/agent_test.go`:
- Around line 160-231: Refactor the three existing tests
(TestDetectOrchestrator, TestDetectOrchestrator_NoneRegistered,
TestDetect_SkipsOrchestrators) into a single table-driven test that iterates
cases for: (a) normal detection of a coding agent vs orchestrator using
reg.Detector().Detect() and Detector.DetectOrchestrator(), (b) no orchestrators
registered returning nil, and (c) handling when an agent's Detect() returns an
error—add a mockAgent case whose Detect() returns an error and assert the
detector skips it and continues; ensure you reference and exercise
detectByRole() behavior by creating entries with role=RoleOrchestrator and role
unset and asserting the expected Type() or nil result for each case.

In `@pkg/agentx/orchestrators/version_helpers.go`:
- Around line 11-23: The versionFromCommand function discards the caller's
context by using context.Background(); update its signature to accept a
context.Context (e.g., versionFromCommand(ctx context.Context, env
agentx.Environment, name string, args ...string)) and replace the env.Exec call
to use that ctx so timeouts/cancellation propagate (env.Exec(ctx, ...)). Also
update the caller DetectVersion to pass its ctx into versionFromCommand so
version detection won't hang if the caller cancels or times out.

---

Nitpick comments:
In `@internal/useragent/useragent.go`:
- Around line 58-71: SetOrchestratorType unnecessarily clears the package-level
cached string even though cached is only used by String() and String() does not
include orchestratorType; remove the misleading cache invalidation by deleting
the line that sets cached = "" in SetOrchestratorType (leave the rest of the
function, including mu locking, orchestratorType assignment, and the early
returns, intact) so that cached is not touched when setting orchestratorType.

In `@pkg/agentx/orchestrators/conductor.go`:
- Around line 31-45: The Detect method in ConductorAgent uses the hardcoded
string "conductor" — replace that literal with the canonical constant by calling
string(agentx.AgentTypeConductor) in the first env check (inside
ConductorAgent.Detect) so the comparison uses the shared AgentTypeConductor
constant rather than a duplicated string; leave the other env checks unchanged.
- Around line 59-61: The Capabilities method on ConductorAgent currently returns
an empty agentx.Capabilities struct; update ConductorAgent.Capabilities to
reflect any supported features (or at minimum add a clear TODO comment) so this
isn't silently all-false—locate the Capabilities method on type ConductorAgent
and either populate the returned agentx.Capabilities fields that Conductor
supports or add a TODO comment above the function (e.g., "// TODO: populate
supported capabilities such as X, Y") explaining what needs to be enabled and
when.

In `@pkg/agentx/orchestrators/openclaw.go`:
- Around line 32-46: Replace the hardcoded "openclaw" string in Detect with the
AgentType constant: change the comparison in OpenClawAgent.Detect to use
string(agentx.AgentTypeOpenClaw) instead of "openclaw"; also search for the same
literal in conductor.go and replace it with string(agentx.AgentTypeOpenClaw) to
keep comparisons synchronized with the AgentTypeOpenClaw constant.

In `@pkg/agentx/registry.go`:
- Around line 93-120: detectByRole currently iterates registry.List() (which
comes from a map) and can pick a non-deterministic winner when multiple agents
match; make the selection deterministic by sorting the agents before iterating
in detectByRole: obtain the slice from registry.List(), sort it by a stable key
(e.g., implement/use an Agent.Priority() integer or fall back to Agent.Type() or
name string) with a stable comparison, then iterate the sorted slice to call
agent.Detect(ctx, env); update detectByRole to perform this sort so Agent
selection is reproducible.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 451426d and 1dde8ec.

📒 Files selected for processing (39)
  • cmd/ox/agent_prime.go
  • cmd/ox/doctor_ecosystem.go
  • internal/api/ledger.go
  • internal/api/repo.go
  • internal/api/repos.go
  • internal/auth/client.go
  • internal/auth/device_flow.go
  • internal/auth/refresh.go
  • internal/doctorapi/client.go
  • internal/lfs/client.go
  • internal/lfs/transfer.go
  • internal/session/summarize.go
  • internal/telemetry/client.go
  • internal/useragent/useragent.go
  • internal/useragent/useragent_test.go
  • internal/uxfriction/client.go
  • pkg/agentx/agent.go
  • pkg/agentx/agent_test.go
  • pkg/agentx/agents/aider.go
  • pkg/agentx/agents/amp.go
  • pkg/agentx/agents/claudecode.go
  • pkg/agentx/agents/cline.go
  • pkg/agentx/agents/codepuppy.go
  • pkg/agentx/agents/cody.go
  • pkg/agentx/agents/continue.go
  • pkg/agentx/agents/copilot.go
  • pkg/agentx/agents/cursor.go
  • pkg/agentx/agents/droid.go
  • pkg/agentx/agents/goose.go
  • pkg/agentx/agents/kiro.go
  • pkg/agentx/agents/opencode.go
  • pkg/agentx/agents/windsurf.go
  • pkg/agentx/orchestrators/conductor.go
  • pkg/agentx/orchestrators/conductor_test.go
  • pkg/agentx/orchestrators/openclaw.go
  • pkg/agentx/orchestrators/openclaw_test.go
  • pkg/agentx/orchestrators/version_helpers.go
  • pkg/agentx/registry.go
  • pkg/agentx/setup/setup.go

Comment thread cmd/ox/doctor_ecosystem.go
Comment thread internal/lfs/client.go
Comment thread pkg/agentx/agent_test.go
Comment thread pkg/agentx/orchestrators/version_helpers.go
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
pkg/agentx/registry.go (1)

101-103: Clarify the env-priority comment for role-based detection.
detectByRole is also used for orchestrators, so the comment should mention the role-specific override env var.

✏️ Suggested comment tweak
-// Each agent's Detect() handles AGENT_ENV priority internally.
+// Each agent's Detect() handles role-specific env priority internally (e.g., AGENT_ENV or ORCHESTRATOR_ENV).
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/agentx/registry.go` around lines 101 - 103, Update the comment for the
detectByRole function to clarify that it returns the first agent detected for
the given AgentRole and that each agent's Detect() respects AGENT_ENV priority
as well as a role-specific override environment variable (so this function is
used for orchestrators too); mention the role-specific override env var name and
that it takes precedence for role-based detection to make the behavior explicit
(reference detectByRole and Detect()).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@pkg/agentx/registry.go`:
- Around line 101-103: Update the comment for the detectByRole function to
clarify that it returns the first agent detected for the given AgentRole and
that each agent's Detect() respects AGENT_ENV priority as well as a
role-specific override environment variable (so this function is used for
orchestrators too); mention the role-specific override env var name and that it
takes precedence for role-based detection to make the behavior explicit
(reference detectByRole and Detect()).

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1dde8ec and f37376a.

📒 Files selected for processing (2)
  • pkg/agentx/agent_test.go
  • pkg/agentx/registry.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • pkg/agentx/agent_test.go

…creation

Co-Authored-By: SageOx <ox@sageox.ai>
@rsnodgrass rsnodgrass merged commit 043f087 into main Feb 24, 2026
1 check passed
@rsnodgrass rsnodgrass deleted the ryan/pty-mode-research branch February 24, 2026 05:56
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.

1 participant