-
Notifications
You must be signed in to change notification settings - Fork 0
feat: add IDE personas for VS Code, Claude Code, and Copilot CLI (v0.2.1) #11
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,108 @@ | ||
| # IDE Personas | ||
|
|
||
| Agents written for VS Code, Claude Code, or the Copilot CLI each expect a | ||
| different native tool set. A `Persona` tells `pytest-codingagents` which | ||
| runtime environment to simulate so your tests run the agent the same way | ||
| the IDE would. | ||
|
|
||
| ## The problem | ||
|
|
||
| An agent like `rpi-agent` is written for VS Code, where `runSubagent` is a | ||
| native tool. In the Copilot SDK headless mode `runSubagent` does not exist, | ||
| so the agent silently falls back to direct implementation — the RPI pipeline | ||
| never fires, and the test proves nothing. | ||
|
|
||
| A persona solves this by: | ||
|
|
||
| 1. **Injecting polyfill tools** — e.g. a Python-side `runSubagent` that | ||
| dispatches registered custom agents as nested SDK runs. | ||
| 2. **Auto-loading custom instructions** — VS Code and Copilot CLI read | ||
| `.github/copilot-instructions.md`; Claude Code reads `CLAUDE.md`. The | ||
| persona does the same, prepending the file to the session's system | ||
| message when `working_directory` is set. | ||
| 3. **Setting IDE context** — adds a system-message fragment so the model | ||
| knows which environment it is in. | ||
|
|
||
| ## Built-in personas | ||
|
|
||
| | Persona | Auto-loaded file | Polyfilled tools | Use for | | ||
| |---|---|---|---| | ||
| | `VSCodePersona` *(default)* | `.github/copilot-instructions.md` | `runSubagent` | VS Code Copilot agents | | ||
| | `CopilotCLIPersona` | `.github/copilot-instructions.md` | none — `task` + `skill` are native | Copilot terminal agents | | ||
| | `ClaudeCodePersona` | `CLAUDE.md` | `task`-dispatch | Claude Code agents | | ||
| | `HeadlessPersona` | nothing | none | Raw SDK baseline | | ||
|
|
||
| ## Usage | ||
|
|
||
| ```python | ||
| from pytest_codingagents import CopilotAgent, VSCodePersona, CopilotCLIPersona, ClaudeCodePersona, HeadlessPersona | ||
|
|
||
| # VS Code agent — auto-loads .github/copilot-instructions.md, polyfills runSubagent | ||
| agent = CopilotAgent( | ||
| persona=VSCodePersona(), | ||
| working_directory=str(workspace), | ||
| custom_agents=my_agents, | ||
| ) | ||
|
|
||
| # Default — VSCodePersona is used automatically | ||
| agent = CopilotAgent(custom_agents=my_agents) | ||
|
|
||
| # Copilot CLI — same instructions file; task+skill already native, no polyfill needed | ||
| agent = CopilotAgent(persona=CopilotCLIPersona(), working_directory=str(workspace)) | ||
|
|
||
| # Claude Code — loads CLAUDE.md, polyfills task-dispatch | ||
| agent = CopilotAgent( | ||
| persona=ClaudeCodePersona(), | ||
| working_directory=str(workspace), | ||
| custom_agents=my_agents, | ||
| ) | ||
|
|
||
| # Headless baseline — no IDE context, no file loaded, no polyfills | ||
| agent = CopilotAgent(persona=HeadlessPersona()) | ||
| ``` | ||
|
|
||
| ## Custom instructions loading | ||
|
|
||
| Custom instruction loading is **automatic and additive**: | ||
|
|
||
| - Fires only when `agent.working_directory` is set | ||
| - Fires only when the target file exists in that directory | ||
| - Prepends the file content to the session system message (before any | ||
| `instructions` you set on the agent) | ||
| - If the file is absent, the persona works exactly as without it | ||
|
|
||
| This means the same test works against a workspace that has | ||
| `.github/copilot-instructions.md` and one that does not — the persona | ||
| adapts silently. | ||
|
|
||
| ## `runSubagent` polyfill | ||
|
|
||
| `VSCodePersona` injects `runSubagent` as a Python-side tool when | ||
| `agent.custom_agents` is non-empty. The tool dispatches the named agent | ||
| as a nested `run_copilot` call, so the model's sub-agent invocations | ||
| produce real results — not stub responses. | ||
|
|
||
| The polyfill is a no-op when `custom_agents` is empty. | ||
|
|
||
| ## Extending personas | ||
|
|
||
| Subclass `Persona` and override `apply()`: | ||
|
|
||
| ```python | ||
| from pytest_codingagents import Persona, CopilotAgent | ||
|
|
||
| class MyPersona(Persona): | ||
| def apply(self, agent, session_config, mapper): | ||
| # Add your tool polyfills or system message additions here | ||
| session_config.setdefault("system_message", {})["content"] = ( | ||
| "Custom context. " + | ||
| session_config.get("system_message", {}).get("content", "") | ||
| ) | ||
|
|
||
| agent = CopilotAgent(persona=MyPersona()) | ||
| ``` | ||
|
|
||
| ## See also | ||
|
|
||
| - [Load from Copilot Config](copilot-config.md) | ||
| - [Tool Control](tool-control.md) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -274,6 +274,31 @@ def _handle_tool_execution_complete(self, event: SessionEvent) -> None: | |
| result_text = tc.result if tc else str(result_data) | ||
| self._turns.append(Turn(role="tool", content=f"[{tool_name}] {result_text or ''}")) | ||
|
|
||
| # ── Subagent recording (used by runSubagent tool handler) ── | ||
|
|
||
| def record_subagent_start(self, name: str) -> None: | ||
| """Record a subagent invocation dispatched via the runSubagent tool.""" | ||
| self._subagent_start_times[name] = time.monotonic() | ||
| self._subagents.append(SubagentInvocation(name=name, status="started")) | ||
|
|
||
| def record_subagent_complete(self, name: str) -> None: | ||
| """Mark a previously started subagent invocation as completed.""" | ||
| start = self._subagent_start_times.pop(name, None) | ||
| duration = (time.monotonic() - start) * 1000 if start else None | ||
| for sa in self._subagents: | ||
| if sa.name == name and sa.status == "started": | ||
| sa.status = "completed" | ||
| sa.duration_ms = duration | ||
| return | ||
|
|
||
| def record_subagent_failed(self, name: str) -> None: | ||
| """Mark a previously started subagent invocation as failed.""" | ||
| self._subagent_start_times.pop(name, None) | ||
| for sa in self._subagents: | ||
| if sa.name == name and sa.status == "started": | ||
| sa.status = "failed" | ||
| return | ||
|
|
||
| # ── Subagent events ── | ||
|
|
||
| def _handle_subagent_selected(self, event: SessionEvent) -> None: | ||
|
|
||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Check failure
Code scanning / CodeQL
Module-level cyclic import Error
Copilot Autofix
AI 11 days ago
In general, to fix this, avoid importing from
pytest_codingagents.copilot.personasat module level inagent.py, even underTYPE_CHECKING, and instead use forward references ortypingutilities (e.g.,typing.TYPE_CHECKINGortyping_extensions) that do not require importing the other module. This breaks the cycle at the source while keeping type hints.The best targeted fix here is to remove the
TYPE_CHECKINGimport ofPersonaand replace the string-annotated"Persona"hints with eithertyping.ForwardRef/typing.get_type_hintspatterns or, more simply and idiomatically for modern Python, by usingfrom __future__ import annotations(which is already present) and atyping.Protocolor atyping.Anyfallback. However, sincefrom __future__ import annotationsis already in place, we can safely use the string"Persona"as a forward reference without needing the import at type-check time, and robust type checkers can be configured to resolve it via stub files or by importingpersonasseparately. To stay within the given snippet and not change other files, the minimal fix is:if TYPE_CHECKING:block that importsPersona.persona: "Persona"with a more generic but safe type such asAny, while keeping the runtime behaviour (_default_persona()still returns aVSCodePersonaobject). This removes the dependency onPersona’s definition order and the cyclic import, without affecting functionality.Concretely, in
src/pytest_codingagents/coplay/agent.py:TYPE_CHECKINGimport ofPersona).personafield’s annotation from"Persona"toAny(which is already imported at line 7), and keep itsdefault_factoryunchanged.No new imports or helpers are needed:
Anyis already imported, and_default_personaremains as-is, still performing a deferred import ofVSCodePersonafor runtime use.