Skip to content

feat(sdk): Copilot CLI tool compatibility (Layer A) + SubagentControl - #60

Open
simurg79 wants to merge 3 commits into
mainfrom
feat/copilot-cli-compat-sdk
Open

feat(sdk): Copilot CLI tool compatibility (Layer A) + SubagentControl#60
simurg79 wants to merge 3 commits into
mainfrom
feat/copilot-cli-compat-sdk

Conversation

@simurg79

@simurg79 simurg79 commented Aug 3, 2026

Copy link
Copy Markdown
Collaborator

What this does

Lets a single .agent.md file run both locally in Copilot CLI and remotely through a
host such as Waldemort, with minimal changes. Previously an agent file naming Copilot
tools (execute, read, edit, ...) meant nothing to a host, so authors maintained
two divergent copies of the same agent.

This is Layer A of a two-layer feature — the SDK half. Layer B is the Waldemort
adapter implementation, in a companion PR (cross-linked below).

Layer A contents

  • Opt-in compat profile. Disabled by default; existing deployments see no behavior
    change. A parity fixture asserts the disabled path is byte-identical to today.
  • Host-registerable adapters + alias resolution at load. Resolving once at load,
    rather than per invocation, means a bad tool name surfaces at startup.
  • Load-time validation of every declared tool name. An unresolvable name is fatal.
    Dropping it silently would let an agent run believing it holds a tool it does not,
    then fail unpredictably much later.
  • New SubagentControl capability. Structured spawn/status/wait records replace
    prose-scraping. All unions discriminate on an explicit ok field, so a caller cannot
    mistake failure for success by testing truthiness. renderSpawnAgentReport is lifted
    to module scope and exported; human-facing prose stays byte-identical, and a
    dedicated prose test pins that.
  • SubagentControl is attached per turn and is absent for service and read-only
    tuner sessions, so absence is meaningful rather than ambiguous. Each operation
    re-checks the turn boundary to prevent use after the turn ends.

Tests

packages/sdk npm run test:unit: 148 passed, 0 failed.
tsc --noEmit clean; npm run build succeeds.

New suites: test/unit/subagent-control-hook.test.mjs,
test/unit/spawn-agent-report-prose.test.mjs, test/unit/copilot-compat.test.mjs.

Dependencies and merge order

This PR is stacked on PR #59 (app default overlay / PLUGIN_DIRS). Because that
branch lives on a fork, GitHub will not accept it as a base, so this PR targets main
and its diff currently also contains #59's two commits. Review only the compat
commit (5dd60be); once #59 merges, this diff collapses to compat-only on its own.

The compat changes in agent-loader.ts and worker.ts interleave with #59's changes
inside the same functions, so the two could not be separated without hand-editing
already-reviewed code. Merge #59 first.

This PR must merge and be released before the Waldemort side can ship. Waldemort
consumes SubagentControl and cannot bump its production pin (currently v0.5.26) until
that release lands.

Not yet shippable

Merging this is necessary but not sufficient. The sequence is: this PR merges → the SDK
publishes a release → Waldemort bumps its manifest, lockfile, and override pin.

Bertan Ari and others added 3 commits July 28, 2026 10:49
An app's `default.agent.md` is force-merged into every session -- both its
prompt and its `tools:`. The only escape hatch was the internal
`pilotswarm-system-agent` prompt-layer kind, which app agents cannot reach, so
authoring one agent that did not want the app-wide instructions meant weakening
the default for every other agent in the deployment.

Agents can now declare `inheritAppDefaults: false`. Such an agent is composed
from the framework base plus its own prompt and tools only, and it is dropped
from the prompt layer manifest. Omitting the field inherits, so every existing
agent is unchanged.

The flag is resolved from the bound agent definition at prompt-composition time
rather than persisted on the session row, matching how agent redefinition is
expected to take effect on the next turn -- so no migration is needed and no
session can carry a stale copy of the decision. A single `inheritsAppDefaults()`
resolver gates all four consumption points (session prompt, subagent prompt,
app-default tool merge, layer manifest) so they cannot drift apart.

This is a composition choice, not a security boundary: invariants that must hold
for every session still belong in the framework base layer, which cannot be
opted out of.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
PLUGIN_DIRS is documented and parsed as a comma-separated list, and the SDK
worker loads every entry (worker.ts Tier 4 iterates config.pluginDirs). The
TUI/portal bootstrap did not: resolvePluginDir() returned dirs[0] and then
overwrote process.env.PLUGIN_DIRS with that single path, so every dir after
the first was discarded before the worker ever saw it.

The failure is silent and the blast radius is bad: an overlay dir holding
default.agent.md would simply not load, dropping the app default prompt and
its force-merged tools with no warning.

resolvePluginDirs() now returns the full list, --plugin accepts a
comma-separated value, and system.md is resolved across all dirs (first match
wins). Branding still reads only the first dir, so single-dir configurations
resolve to exactly the same values as before.

The same truncation in web/bin/serve.js --plugin handling is fixed too.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
An agent file written for Copilot CLI names tools like `execute` and `read`.
Those names mean nothing to a host such as Waldemort, so the same file could
not run in both places without being rewritten for each. Authors kept two
divergent copies.

Introduce an opt-in compatibility profile. When enabled, an agent may declare
Copilot tool names; the host registers adapters, and aliases resolve at load.
Resolution happens once at load rather than per invocation so a bad tool name
surfaces at startup instead of mid-turn. Every declared name is validated
against what the host actually registered, and an unresolvable name is fatal:
silently dropping it would let an agent run believing it has a tool it does
not, and fail unpredictably much later. The profile is opt-in, so existing
deployments see no behavior change.

Add a SubagentControl capability. Callers previously had to scrape spawn and
status results out of human-readable prose, which broke whenever the wording
changed. Expose structured records instead, discriminated on an explicit `ok`
field so a caller cannot mistake failure for success by testing truthiness.
`renderSpawnAgentReport` moves to module scope and is exported, keeping the
existing prose byte-identical for human-facing paths while the structured
path bypasses it entirely.

The capability is attached per turn and is absent for service and read-only
tuner sessions, so its absence is meaningful rather than ambiguous. Each
operation re-checks the turn boundary to prevent use after the turn ends.

packages/sdk test:unit: 148 passed, 0 failed.
tsc --noEmit clean; npm run build succeeds.
@simurg79

simurg79 commented Aug 3, 2026

Copy link
Copy Markdown
Collaborator Author

Companion PR (Layer B, Waldemort adapters): microsoft/waldemort#37 — https://github.com/microsoft/waldemort/pull/37%0A%0AThat PR consumes the SubagentControl capability added here and cannot ship until this merges and the SDK publishes a release.

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