feat(cpm): agent_ref - compose APM-deployed agents into teams
Empirically verified: oh-my-opencode-slim preset entries keyed to an APM-deployed native OpenCode agent (.opencode/agents/*.md) reconfigure it in place - model override works, no duplicate agents are created. - new optional role field 'agent_ref: <name>': persona (prompt, identity) comes from the APM primitive; cpm only resolves model_class locally and emits a preset entry under the deployed agent's name - validation: warns when agent_ref target is missing from .opencode/agents/ (apm install not run yet) and when 'purpose' is redundantly set on agent_ref roles - collision warning: inline roles shadowing deployed native agents suggest using agent_ref instead - microsoft-design-review example: reviewer role now uses agent_ref: design-reviewer, showcasing both role forms - README: new section on the relationship to APM agent primitives - blog-review.md: verified findings as material for a blog addendum
This commit is contained in:
parent
dd72fc4520
commit
9a3b8d62f3
4 changed files with 98 additions and 12 deletions
|
|
@ -579,3 +579,20 @@ Direkt vor dem ersten team-profile-Codeblock (Abschnitt „Die APM-OMOS-Idee"):
|
||||||
Ersatzfassung:
|
Ersatzfassung:
|
||||||
> „Spawnfile v0.1 besitzt derzeit keinen gebündelten OpenCode-Adapter. OpenCode wird als explorativer Runtime-Kandidat geführt; ein natives OpenCode-Artefakt wird daher aktuell nicht erzeugt."
|
> „Spawnfile v0.1 besitzt derzeit keinen gebündelten OpenCode-Adapter. OpenCode wird als explorativer Runtime-Kandidat geführt; ein natives OpenCode-Artefakt wird daher aktuell nicht erzeugt."
|
||||||
Zusätzlich (aus unserer Code-Analyse): In v0.1 implementiert kein Adapter `compileTeam()` – Teams degradieren überall zu Kontext-Dokumenten.
|
Zusätzlich (aus unserer Code-Analyse): In v0.1 implementiert kein Adapter `compileTeam()` – Teams degradieren überall zu Kontext-Dokumenten.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Blog-Nachtrag-Material: APM-Agent-Primitives als Persona-Quelle (empirisch verifiziert)
|
||||||
|
|
||||||
|
Aus dem Praxistest mit dem offiziellen microsoft/apm-sample-package (August 2026):
|
||||||
|
|
||||||
|
**Befund:** Ein Preset-Eintrag, der auf den Namen eines APM-deployten nativen OpenCode-Agenten zielt, konfiguriert diesen Agenten – inklusive Modell-Override. Kein Duplikat, die Persona aus der deployten Datei bleibt erhalten. Verifiziert am design-reviewer-Agenten: injiziertes `ollama/qwen3.5:9b` wurde zur Laufzeit gemeldet („Design-Reviewer · Qwen 3.5 9B").
|
||||||
|
|
||||||
|
**Konsequenz für das CPM-Format:** Die Rollen-Definition hat zwei Formen:
|
||||||
|
|
||||||
|
1. **Inline-Persona** (`purpose` + `capabilities`) für Packages ohne eigene Agent-Primitives
|
||||||
|
2. **`agent_ref`** für Packages mit `.apm/agents/`: Die Rolle bindet den deployten Agenten ins Team, `model_class` löst das Modell lokal auf
|
||||||
|
|
||||||
|
Damit schrumpft die CPM-Rolle bei Form B auf ihr Minimum: Team-Slot + Modellklasse (+ optionale Einschränkung der Whitelist). Prompt und Tools kommen aus dem Primitive – eine Quelle der Wahrheit, „tighten only".
|
||||||
|
|
||||||
|
**Für den Blogpost:** Der Abschnitt „Was OMOS tatsächlich begrenzen kann" kann um diesen Befund ergänzt werden; außerdem ist der Design-Review-Beispiel-Flow jetzt vollständig end-to-end gelaufen (cpm setup → apm install → /preset → Delegation → Modell-Verifikation).
|
||||||
|
|
|
||||||
|
|
@ -114,10 +114,35 @@ roles:
|
||||||
capabilities:
|
capabilities:
|
||||||
mcps: [websearch, openviking] # MCP allowlist ([] = none)
|
mcps: [websearch, openviking] # MCP allowlist ([] = none)
|
||||||
skills: [job-application] # skill allowlist ([] = none)
|
skills: [job-application] # skill allowlist ([] = none)
|
||||||
|
|
||||||
|
- id: reviewer # Form B: bind an APM-deployed agent
|
||||||
|
agent_ref: design-reviewer # persona comes from .opencode/agents/
|
||||||
|
model_class: high-reasoning # resolved locally, injected via preset
|
||||||
```
|
```
|
||||||
|
|
||||||
The `runtime_agent` field is deliberately harness-neutral: the adapter decides how the role is represented in the target harness. Builtins recognized by the oh-my-opencode-slim adapter: `orchestrator`, `oracle`, `librarian`, `explorer`, `fixer`, `designer`, `council`, `observer`. Everything else (or `runtime_agent: custom`) creates a custom agent including `prompt` and `orchestratorPrompt` derived from the `purpose` field.
|
The `runtime_agent` field is deliberately harness-neutral: the adapter decides how the role is represented in the target harness. Builtins recognized by the oh-my-opencode-slim adapter: `orchestrator`, `oracle`, `librarian`, `explorer`, `fixer`, `designer`, `council`, `observer`. Everything else (or `runtime_agent: custom`) creates a custom agent including `prompt` and `orchestratorPrompt` derived from the `purpose` field.
|
||||||
|
|
||||||
|
### Relationship to APM agent primitives
|
||||||
|
|
||||||
|
APM packages can carry their own agent definitions (`.apm/agents/*.agent.md`, deployed to `.opencode/agents/`). A CPM team profile composes these deployed agents instead of duplicating them:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
roles:
|
||||||
|
- id: reviewer
|
||||||
|
agent_ref: design-reviewer # persona comes from the APM-deployed primitive
|
||||||
|
model_class: high-reasoning # model class resolved locally, injected via preset
|
||||||
|
```
|
||||||
|
|
||||||
|
With `agent_ref`:
|
||||||
|
|
||||||
|
- **Persona** (prompt, identity) comes from the APM primitive – `purpose` is ignored. One source of truth.
|
||||||
|
- **Model** is resolved by the CPM from your local mapping and injected via the preset entry keyed to the agent's name. Verified empirically: oh-my-opencode-slim presets reconfigure APM-deployed native agents (model override works, no duplicate agents are created).
|
||||||
|
- The team may only **restrict** what the primitive declares, never redefine it – same tighten-only philosophy as APM policies.
|
||||||
|
|
||||||
|
Roles without `agent_ref` use the inline form (`purpose` + `capabilities`) for packages that ship no agent primitives. `cpm check` warns when an inline role would shadow a deployed native agent – consider `agent_ref` instead.
|
||||||
|
|
||||||
|
Note: if an APM agent primitive freezes a concrete model you do not have, prefer upstream primitives that omit `model:` (like [microsoft/apm-sample-package](https://github.com/microsoft/apm-sample-package) does) and let the CPM resolve it locally via `model_class`.
|
||||||
|
|
||||||
### Mapping formats
|
### Mapping formats
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
|
|
@ -143,7 +168,7 @@ All extra fields are passed through 1:1 into the agent entry of the preset.
|
||||||
| Example | Based on | Team |
|
| Example | Based on | Team |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `examples/package/` | Own `job-application` package (OpenViking + ShareLaTeX) | `acme.job-applications` – Researcher, Writer, Checker, Notifier |
|
| `examples/package/` | Own `job-application` package (OpenViking + ShareLaTeX) | `acme.job-applications` – Researcher, Writer, Checker, Notifier |
|
||||||
| `examples/microsoft-design-review/` | Official [microsoft/apm-sample-package](https://github.com/microsoft/apm-sample-package) | `acme.design-review` – Reviewer (Oracle), Style-Checker (Fixer), Accessibility-Auditor |
|
| `examples/microsoft-design-review/` | Official [microsoft/apm-sample-package](https://github.com/microsoft/apm-sample-package) | `acme.design-review` – Reviewer (`agent_ref` → deployed design-reviewer), Style-Checker (Fixer), Accessibility-Auditor |
|
||||||
| `examples/microsoft-issue-autopilot/` | Official [`apm-issue-autopilot`](https://github.com/microsoft/apm/tree/main/packages/apm-issue-autopilot) from the microsoft/apm repo | `acme.issue-autopilot` – Triager, Shepherd, PR-Writer, Reviewer (+ GitHub MCP) |
|
| `examples/microsoft-issue-autopilot/` | Official [`apm-issue-autopilot`](https://github.com/microsoft/apm/tree/main/packages/apm-issue-autopilot) from the microsoft/apm repo | `acme.issue-autopilot` – Triager, Shepherd, PR-Writer, Reviewer (+ GitHub MCP) |
|
||||||
|
|
||||||
The two Microsoft examples are thin CPM wrappers: their own `apm.yml` pulls the official package as a versioned APM dependency (`microsoft/apm-sample-package#v1.0.0` or monorepo subpath `microsoft/apm/packages/apm-issue-autopilot`) and only adds the team recommendation.
|
The two Microsoft examples are thin CPM wrappers: their own `apm.yml` pulls the official package as a versioned APM dependency (`microsoft/apm-sample-package#v1.0.0` or monorepo subpath `microsoft/apm/packages/apm-issue-autopilot`) and only adds the team recommendation.
|
||||||
|
|
|
||||||
|
|
@ -142,12 +142,23 @@ def sanitize_preset_name(team_id: str) -> str:
|
||||||
# Translation
|
# Translation
|
||||||
|
|
||||||
|
|
||||||
def build_preset(profile: dict, mapping: dict) -> tuple[dict, dict, list[str]]:
|
def deployed_native_agents(package_dir: Path) -> set[str]:
|
||||||
|
"""Names of APM-deployed native agents (.opencode/agents/*.md)."""
|
||||||
|
agents_dir = package_dir / ".opencode" / "agents"
|
||||||
|
if not agents_dir.exists():
|
||||||
|
return set()
|
||||||
|
return {p.stem for p in agents_dir.glob("*.md")}
|
||||||
|
|
||||||
|
|
||||||
|
def build_preset(profile: dict, mapping: dict,
|
||||||
|
package_dir: Path | None = None) -> tuple[dict, dict, list[str]]:
|
||||||
"""Returns (preset, custom_agents, warnings)."""
|
"""Returns (preset, custom_agents, warnings)."""
|
||||||
preset: dict = {}
|
preset: dict = {}
|
||||||
custom_agents: dict = {}
|
custom_agents: dict = {}
|
||||||
warnings: list[str] = []
|
warnings: list[str] = []
|
||||||
|
|
||||||
|
native = deployed_native_agents(package_dir) if package_dir else set()
|
||||||
|
|
||||||
for role in profile["roles"]:
|
for role in profile["roles"]:
|
||||||
role_id = role.get("id")
|
role_id = role.get("id")
|
||||||
if not role_id:
|
if not role_id:
|
||||||
|
|
@ -170,6 +181,30 @@ def build_preset(profile: dict, mapping: dict) -> tuple[dict, dict, list[str]]:
|
||||||
skills = list(capabilities.get("skills", []) or [])
|
skills = list(capabilities.get("skills", []) or [])
|
||||||
purpose = str(role.get("purpose", "")).strip()
|
purpose = str(role.get("purpose", "")).strip()
|
||||||
|
|
||||||
|
# Form B: bind the role to an APM-deployed native agent.
|
||||||
|
agent_ref = role.get("agent_ref")
|
||||||
|
|
||||||
|
if agent_ref:
|
||||||
|
if native and agent_ref not in native:
|
||||||
|
warnings.append(
|
||||||
|
f"Role '{role_id}': agent_ref '{agent_ref}' not found in "
|
||||||
|
f".opencode/agents/ ({', '.join(sorted(native)) or 'empty'}). "
|
||||||
|
"Run 'apm install' first?"
|
||||||
|
)
|
||||||
|
preset[agent_ref] = {
|
||||||
|
**agent_fields,
|
||||||
|
"mcps": mcps,
|
||||||
|
"skills": skills,
|
||||||
|
}
|
||||||
|
if not purpose:
|
||||||
|
pass # persona comes from the APM primitive; nothing to do
|
||||||
|
else:
|
||||||
|
warnings.append(
|
||||||
|
f"Role '{role_id}': 'purpose' ignored - with agent_ref the "
|
||||||
|
"persona (prompt) comes from the APM agent primitive."
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
runtime_agent = role.get("runtime_agent", role_id)
|
runtime_agent = role.get("runtime_agent", role_id)
|
||||||
if runtime_agent == "custom":
|
if runtime_agent == "custom":
|
||||||
# Explicitly marked as custom agent -> use the role's own name.
|
# Explicitly marked as custom agent -> use the role's own name.
|
||||||
|
|
@ -222,6 +257,20 @@ def build_preset(profile: dict, mapping: dict) -> tuple[dict, dict, list[str]]:
|
||||||
"entry its model stays unchanged."
|
"entry its model stays unchanged."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Inline (non-agent_ref) roles colliding with deployed native agents
|
||||||
|
# would create OMOS custom agents that shadow the APM-deployed ones.
|
||||||
|
for role in profile["roles"]:
|
||||||
|
if role.get("agent_ref"):
|
||||||
|
continue
|
||||||
|
role_id = role.get("id")
|
||||||
|
runtime_agent = role.get("runtime_agent", role_id)
|
||||||
|
if runtime_agent == "custom" and native and role_id in native:
|
||||||
|
warnings.append(
|
||||||
|
f"Role '{role_id}' would create a custom agent shadowing the "
|
||||||
|
f"APM-deployed native agent '{role_id}'. Consider "
|
||||||
|
f"'agent_ref: {role_id}' instead."
|
||||||
|
)
|
||||||
|
|
||||||
return preset, custom_agents, warnings
|
return preset, custom_agents, warnings
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -393,7 +442,7 @@ def cmd_render(args: argparse.Namespace) -> int:
|
||||||
profile, profile_path = load_team_profile(package_dir, args.team_file)
|
profile, profile_path = load_team_profile(package_dir, args.team_file)
|
||||||
mapping = load_mapping(Path(args.mapping) if args.mapping else None)
|
mapping = load_mapping(Path(args.mapping) if args.mapping else None)
|
||||||
|
|
||||||
preset, custom_agents, warnings = build_preset(profile, mapping)
|
preset, custom_agents, warnings = build_preset(profile, mapping, package_dir)
|
||||||
|
|
||||||
output = Path(args.output) if args.output else package_dir / DEFAULT_OUTPUT
|
output = Path(args.output) if args.output else package_dir / DEFAULT_OUTPUT
|
||||||
if output.exists():
|
if output.exists():
|
||||||
|
|
@ -430,6 +479,7 @@ def cmd_render(args: argparse.Namespace) -> int:
|
||||||
"roles": {
|
"roles": {
|
||||||
r.get("id"): {
|
r.get("id"): {
|
||||||
"runtimeAgent": r.get("runtime_agent", r.get("id")),
|
"runtimeAgent": r.get("runtime_agent", r.get("id")),
|
||||||
|
"agentRef": r.get("agent_ref"),
|
||||||
"modelClass": r.get("model_class"),
|
"modelClass": r.get("model_class"),
|
||||||
"resolvedModel": mapping[r["model_class"]],
|
"resolvedModel": mapping[r["model_class"]],
|
||||||
"mcps": (r.get("capabilities", {}) or {}).get("mcps", []),
|
"mcps": (r.get("capabilities", {}) or {}).get("mcps", []),
|
||||||
|
|
|
||||||
|
|
@ -20,16 +20,10 @@ roles:
|
||||||
skills: []
|
skills: []
|
||||||
|
|
||||||
- id: reviewer
|
- id: reviewer
|
||||||
purpose: >-
|
# persona (prompt, identity) comes from the APM-deployed primitive
|
||||||
Performs the design review following the design-review.prompt.md
|
# .opencode/agents/design-reviewer.md - created by 'apm install'
|
||||||
template. Evaluates UI consistency against the
|
agent_ref: design-reviewer
|
||||||
design-standards.instructions.md. Reports findings with severity,
|
|
||||||
no unauthorized code changes.
|
|
||||||
runtime_agent: oracle # deep review = oracle profile
|
|
||||||
model_class: high-reasoning
|
model_class: high-reasoning
|
||||||
capabilities:
|
|
||||||
mcps: []
|
|
||||||
skills: [style-checker]
|
|
||||||
|
|
||||||
- id: style-checker
|
- id: style-checker
|
||||||
purpose: >-
|
purpose: >-
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue