xdog-coding 2.0.1__tar.gz → 2.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {xdog_coding-2.0.1 → xdog_coding-2.1.0}/PKG-INFO +5 -5
- {xdog_coding-2.0.1 → xdog_coding-2.1.0}/pyproject.toml +4 -4
- {xdog_coding-2.0.1 → xdog_coding-2.1.0}/src/xdog/coding/core/agent_session.py +40 -8
- {xdog_coding-2.0.1 → xdog_coding-2.1.0}/src/xdog/coding/core/sdk.py +27 -1
- {xdog_coding-2.0.1 → xdog_coding-2.1.0}/src/xdog/coding/core/system_prompt.py +13 -1
- {xdog_coding-2.0.1 → xdog_coding-2.1.0}/tests/test_slash_skills.py +85 -5
- {xdog_coding-2.0.1 → xdog_coding-2.1.0}/.gitignore +0 -0
- {xdog_coding-2.0.1 → xdog_coding-2.1.0}/LICENSE +0 -0
- {xdog_coding-2.0.1 → xdog_coding-2.1.0}/LICENSE-EXCEPTION.md +0 -0
- {xdog_coding-2.0.1 → xdog_coding-2.1.0}/README.md +0 -0
- {xdog_coding-2.0.1 → xdog_coding-2.1.0}/src/xdog/coding/__init__.py +0 -0
- {xdog_coding-2.0.1 → xdog_coding-2.1.0}/src/xdog/coding/cli/__init__.py +0 -0
- {xdog_coding-2.0.1 → xdog_coding-2.1.0}/src/xdog/coding/cli/args.py +0 -0
- {xdog_coding-2.0.1 → xdog_coding-2.1.0}/src/xdog/coding/cli/config_selector.py +0 -0
- {xdog_coding-2.0.1 → xdog_coding-2.1.0}/src/xdog/coding/cli/file_processor.py +0 -0
- {xdog_coding-2.0.1 → xdog_coding-2.1.0}/src/xdog/coding/cli/initial_message.py +0 -0
- {xdog_coding-2.0.1 → xdog_coding-2.1.0}/src/xdog/coding/cli/list_models.py +0 -0
- {xdog_coding-2.0.1 → xdog_coding-2.1.0}/src/xdog/coding/cli/session_picker.py +0 -0
- {xdog_coding-2.0.1 → xdog_coding-2.1.0}/src/xdog/coding/config.py +0 -0
- {xdog_coding-2.0.1 → xdog_coding-2.1.0}/src/xdog/coding/core/__init__.py +0 -0
- {xdog_coding-2.0.1 → xdog_coding-2.1.0}/src/xdog/coding/core/bash_executor.py +0 -0
- {xdog_coding-2.0.1 → xdog_coding-2.1.0}/src/xdog/coding/core/compaction/__init__.py +0 -0
- {xdog_coding-2.0.1 → xdog_coding-2.1.0}/src/xdog/coding/core/compaction/compaction.py +0 -0
- {xdog_coding-2.0.1 → xdog_coding-2.1.0}/src/xdog/coding/core/compaction/utils.py +0 -0
- {xdog_coding-2.0.1 → xdog_coding-2.1.0}/src/xdog/coding/core/defaults.py +0 -0
- {xdog_coding-2.0.1 → xdog_coding-2.1.0}/src/xdog/coding/core/event_bus.py +0 -0
- {xdog_coding-2.0.1 → xdog_coding-2.1.0}/src/xdog/coding/core/exec_utils.py +0 -0
- {xdog_coding-2.0.1 → xdog_coding-2.1.0}/src/xdog/coding/core/extensions/__init__.py +0 -0
- {xdog_coding-2.0.1 → xdog_coding-2.1.0}/src/xdog/coding/core/extensions/loader.py +0 -0
- {xdog_coding-2.0.1 → xdog_coding-2.1.0}/src/xdog/coding/core/extensions/runner.py +0 -0
- {xdog_coding-2.0.1 → xdog_coding-2.1.0}/src/xdog/coding/core/extensions/types.py +0 -0
- {xdog_coding-2.0.1 → xdog_coding-2.1.0}/src/xdog/coding/core/keybindings.py +0 -0
- {xdog_coding-2.0.1 → xdog_coding-2.1.0}/src/xdog/coding/core/messages.py +0 -0
- {xdog_coding-2.0.1 → xdog_coding-2.1.0}/src/xdog/coding/core/prompt_templates.py +0 -0
- {xdog_coding-2.0.1 → xdog_coding-2.1.0}/src/xdog/coding/core/resource_loader.py +0 -0
- {xdog_coding-2.0.1 → xdog_coding-2.1.0}/src/xdog/coding/core/session_manager.py +0 -0
- {xdog_coding-2.0.1 → xdog_coding-2.1.0}/src/xdog/coding/core/settings_manager.py +0 -0
- {xdog_coding-2.0.1 → xdog_coding-2.1.0}/src/xdog/coding/core/slash_commands.py +0 -0
- {xdog_coding-2.0.1 → xdog_coding-2.1.0}/src/xdog/coding/core/timings.py +0 -0
- {xdog_coding-2.0.1 → xdog_coding-2.1.0}/src/xdog/coding/core/tools/__init__.py +0 -0
- {xdog_coding-2.0.1 → xdog_coding-2.1.0}/src/xdog/coding/main.py +0 -0
- {xdog_coding-2.0.1 → xdog_coding-2.1.0}/src/xdog/coding/modes/__init__.py +0 -0
- {xdog_coding-2.0.1 → xdog_coding-2.1.0}/src/xdog/coding/modes/interactive/__init__.py +0 -0
- {xdog_coding-2.0.1 → xdog_coding-2.1.0}/src/xdog/coding/modes/interactive/components/__init__.py +0 -0
- {xdog_coding-2.0.1 → xdog_coding-2.1.0}/src/xdog/coding/modes/interactive/components/assistant_message.py +0 -0
- {xdog_coding-2.0.1 → xdog_coding-2.1.0}/src/xdog/coding/modes/interactive/components/chat_log.py +0 -0
- {xdog_coding-2.0.1 → xdog_coding-2.1.0}/src/xdog/coding/modes/interactive/components/custom_editor.py +0 -0
- {xdog_coding-2.0.1 → xdog_coding-2.1.0}/src/xdog/coding/modes/interactive/components/footer.py +0 -0
- {xdog_coding-2.0.1 → xdog_coding-2.1.0}/src/xdog/coding/modes/interactive/components/tool_execution.py +0 -0
- {xdog_coding-2.0.1 → xdog_coding-2.1.0}/src/xdog/coding/modes/interactive/components/user_message.py +0 -0
- {xdog_coding-2.0.1 → xdog_coding-2.1.0}/src/xdog/coding/modes/interactive/interactive_mode.py +0 -0
- {xdog_coding-2.0.1 → xdog_coding-2.1.0}/src/xdog/coding/modes/interactive/theme.py +0 -0
- {xdog_coding-2.0.1 → xdog_coding-2.1.0}/src/xdog/coding/modes/print_mode.py +0 -0
- {xdog_coding-2.0.1 → xdog_coding-2.1.0}/src/xdog/coding/modes/rpc/__init__.py +0 -0
- {xdog_coding-2.0.1 → xdog_coding-2.1.0}/src/xdog/coding/modes/rpc/rpc_mode.py +0 -0
- {xdog_coding-2.0.1 → xdog_coding-2.1.0}/src/xdog/coding/py.typed +0 -0
- {xdog_coding-2.0.1 → xdog_coding-2.1.0}/src/xdog/coding/utils/__init__.py +0 -0
- {xdog_coding-2.0.1 → xdog_coding-2.1.0}/src/xdog/coding/utils/frontmatter.py +0 -0
- {xdog_coding-2.0.1 → xdog_coding-2.1.0}/src/xdog/coding/utils/git.py +0 -0
- {xdog_coding-2.0.1 → xdog_coding-2.1.0}/src/xdog/coding/utils/mime.py +0 -0
- {xdog_coding-2.0.1 → xdog_coding-2.1.0}/src/xdog/coding/utils/shell.py +0 -0
- {xdog_coding-2.0.1 → xdog_coding-2.1.0}/src/xdog/coding/utils/sleep.py +0 -0
- {xdog_coding-2.0.1 → xdog_coding-2.1.0}/tests/test_agent_session.py +0 -0
- {xdog_coding-2.0.1 → xdog_coding-2.1.0}/tests/test_cli_args.py +0 -0
- {xdog_coding-2.0.1 → xdog_coding-2.1.0}/tests/test_cli_config_selector.py +0 -0
- {xdog_coding-2.0.1 → xdog_coding-2.1.0}/tests/test_cli_file_processor.py +0 -0
- {xdog_coding-2.0.1 → xdog_coding-2.1.0}/tests/test_coding_config.py +0 -0
- {xdog_coding-2.0.1 → xdog_coding-2.1.0}/tests/test_event_bus.py +0 -0
- {xdog_coding-2.0.1 → xdog_coding-2.1.0}/tests/test_footer_context.py +0 -0
- {xdog_coding-2.0.1 → xdog_coding-2.1.0}/tests/test_interactive_mode.py +0 -0
- {xdog_coding-2.0.1 → xdog_coding-2.1.0}/tests/test_messages.py +0 -0
- {xdog_coding-2.0.1 → xdog_coding-2.1.0}/tests/test_prompt_templates.py +0 -0
- {xdog_coding-2.0.1 → xdog_coding-2.1.0}/tests/test_resource_loader.py +0 -0
- {xdog_coding-2.0.1 → xdog_coding-2.1.0}/tests/test_rpc_mode.py +0 -0
- {xdog_coding-2.0.1 → xdog_coding-2.1.0}/tests/test_session_manager.py +0 -0
- {xdog_coding-2.0.1 → xdog_coding-2.1.0}/tests/test_session_smoke.py +0 -0
- {xdog_coding-2.0.1 → xdog_coding-2.1.0}/tests/test_settings_manager.py +0 -0
- {xdog_coding-2.0.1 → xdog_coding-2.1.0}/tests/test_slash_commands.py +0 -0
- {xdog_coding-2.0.1 → xdog_coding-2.1.0}/tests/test_system_prompt.py +0 -0
- {xdog_coding-2.0.1 → xdog_coding-2.1.0}/tests/test_timings.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: xdog-coding
|
|
3
|
-
Version: 2.0
|
|
3
|
+
Version: 2.1.0
|
|
4
4
|
Summary: Interactive coding agent CLI
|
|
5
5
|
Project-URL: Homepage, https://xdog.942295.xyz
|
|
6
6
|
Project-URL: Repository, https://github.com/litterzhang/x-dog
|
|
@@ -21,9 +21,9 @@ Requires-Python: >=3.12
|
|
|
21
21
|
Requires-Dist: click>=8.1.0
|
|
22
22
|
Requires-Dist: pydantic>=2.0
|
|
23
23
|
Requires-Dist: pyyaml>=6.0
|
|
24
|
-
Requires-Dist: xdog-agent
|
|
25
|
-
Requires-Dist: xdog-ai
|
|
26
|
-
Requires-Dist: xdog-tui
|
|
24
|
+
Requires-Dist: xdog-agent>=2.1.0
|
|
25
|
+
Requires-Dist: xdog-ai>=2.1.0
|
|
26
|
+
Requires-Dist: xdog-tui>=2.1.0
|
|
27
27
|
Description-Content-Type: text/markdown
|
|
28
28
|
|
|
29
29
|
# xdog-coding
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "xdog-coding"
|
|
7
|
-
version = "2.0
|
|
7
|
+
version = "2.1.0"
|
|
8
8
|
description = "Interactive coding agent CLI"
|
|
9
9
|
requires-python = ">=3.12"
|
|
10
10
|
classifiers = [
|
|
@@ -21,9 +21,9 @@ readme = "README.md"
|
|
|
21
21
|
license = "AGPL-3.0-or-later"
|
|
22
22
|
license-files = ["LICENSE", "LICENSE-EXCEPTION.md"]
|
|
23
23
|
dependencies = [
|
|
24
|
-
"xdog-ai",
|
|
25
|
-
"xdog-agent",
|
|
26
|
-
"xdog-tui",
|
|
24
|
+
"xdog-ai>=2.1.0",
|
|
25
|
+
"xdog-agent>=2.1.0",
|
|
26
|
+
"xdog-tui>=2.1.0",
|
|
27
27
|
"pyyaml>=6.0",
|
|
28
28
|
"click>=8.1.0",
|
|
29
29
|
"pydantic>=2.0",
|
|
@@ -94,10 +94,14 @@ class AgentSession:
|
|
|
94
94
|
what the format's reference clients do, and it is a one-way door: a
|
|
95
95
|
message cannot be taken back out of a conversation, so the body keeps
|
|
96
96
|
occupying the window for the rest of the session whether or not it is
|
|
97
|
-
still relevant.
|
|
98
|
-
|
|
97
|
+
still relevant. Keeping it in the system prompt is what makes
|
|
98
|
+
deactivation possible at all.
|
|
99
|
+
|
|
100
|
+
`Agent.set_skills` does the placing, so that reasoning now lives in one
|
|
101
|
+
place instead of being re-derived by every product that has skills.
|
|
99
102
|
"""
|
|
100
103
|
self._active_skills = self._active_skills | {slug}
|
|
104
|
+
self._push_skills()
|
|
101
105
|
self._rebuild_system_prompt()
|
|
102
106
|
|
|
103
107
|
def deactivate_skill(self, slug: str) -> bool:
|
|
@@ -105,6 +109,7 @@ class AgentSession:
|
|
|
105
109
|
if slug not in self._active_skills:
|
|
106
110
|
return False
|
|
107
111
|
self._active_skills = self._active_skills - {slug}
|
|
112
|
+
self._push_skills()
|
|
108
113
|
self._rebuild_system_prompt()
|
|
109
114
|
return True
|
|
110
115
|
|
|
@@ -159,6 +164,7 @@ class AgentSession:
|
|
|
159
164
|
|
|
160
165
|
if expired:
|
|
161
166
|
self._active_skills = self._active_skills - expired
|
|
167
|
+
self._push_skills()
|
|
162
168
|
self._rebuild_system_prompt()
|
|
163
169
|
logger.debug("expired turn-scoped skills: %s", sorted(expired))
|
|
164
170
|
|
|
@@ -287,6 +293,33 @@ class AgentSession:
|
|
|
287
293
|
|
|
288
294
|
# -- Internal --
|
|
289
295
|
|
|
296
|
+
def _push_skills(self) -> None:
|
|
297
|
+
"""Tell the Agent which skills are active; it decides where they go.
|
|
298
|
+
|
|
299
|
+
The index and the bodies are both its business now — this only says
|
|
300
|
+
*which*, which is the part a product legitimately differs on.
|
|
301
|
+
"""
|
|
302
|
+
try:
|
|
303
|
+
self.agent.set_active_skills(sorted(self._active_skills))
|
|
304
|
+
except Exception:
|
|
305
|
+
logger.debug("could not push active skills to the agent", exc_info=True)
|
|
306
|
+
|
|
307
|
+
def _native_tool_calls(self) -> bool:
|
|
308
|
+
"""Whether this model gets tool definitions through the API.
|
|
309
|
+
|
|
310
|
+
Reads the same answer the Agent uses to decide whether to send them, so
|
|
311
|
+
the two cannot disagree — a model whose tools are withheld from the API
|
|
312
|
+
while the prompt stays silent about them has no tools at all, and
|
|
313
|
+
nothing would report that.
|
|
314
|
+
|
|
315
|
+
Unknown answers False, so the tools stay described. Both halves then
|
|
316
|
+
fire, which wastes a few hundred tokens and cannot leave the model
|
|
317
|
+
unable to act.
|
|
318
|
+
"""
|
|
319
|
+
from xdog.agent.helpers import model_supports_tool_calls
|
|
320
|
+
|
|
321
|
+
return model_supports_tool_calls(self.agent.state.model or "") is True
|
|
322
|
+
|
|
290
323
|
def _rebuild_system_prompt(self) -> None:
|
|
291
324
|
"""Rebuild and set the system prompt from config and tools."""
|
|
292
325
|
from xdog.coding.config import PlatformInfo, RuntimeConfig
|
|
@@ -310,6 +343,7 @@ class AgentSession:
|
|
|
310
343
|
),
|
|
311
344
|
tool_defs,
|
|
312
345
|
extra_context=_skills_context(self._active_skills),
|
|
346
|
+
native_tool_calls=self._native_tool_calls(),
|
|
313
347
|
)
|
|
314
348
|
self.agent.set_system_prompt(prompt)
|
|
315
349
|
|
|
@@ -356,16 +390,14 @@ def _skills_context(active: frozenset[str] = frozenset()) -> str:
|
|
|
356
390
|
read is not a reason to start without a system prompt.
|
|
357
391
|
"""
|
|
358
392
|
try:
|
|
359
|
-
from xdog.agent.skills import render_skill_body
|
|
360
393
|
from xdog.coding.core.slash_commands import skill_manager
|
|
361
394
|
|
|
362
395
|
manager = skill_manager()
|
|
363
396
|
summary = manager.skills_summary()
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
bodies.append(f"## Active skill: {skill.name}\n\n{render_skill_body(skill)}")
|
|
397
|
+
# Bodies are the Agent's to place (`set_skills`); this section carries
|
|
398
|
+
# only the standing note about them. Rendering them here as well was how
|
|
399
|
+
# three packages ended up with three answers to the same question.
|
|
400
|
+
bodies = ["(active: " + ", ".join(sorted(active)) + ")"] if active else []
|
|
369
401
|
except Exception:
|
|
370
402
|
logger.debug("could not build the skills section of the system prompt", exc_info=True)
|
|
371
403
|
return ""
|
|
@@ -7,6 +7,11 @@ and session management.
|
|
|
7
7
|
|
|
8
8
|
from __future__ import annotations
|
|
9
9
|
|
|
10
|
+
from typing import TYPE_CHECKING
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from xdog.agent.skills import SkillManager
|
|
14
|
+
|
|
10
15
|
import logging
|
|
11
16
|
from dataclasses import dataclass
|
|
12
17
|
from pathlib import Path
|
|
@@ -149,19 +154,23 @@ def create_agent_session(options: CreateSessionOptions | None = None) -> CreateS
|
|
|
149
154
|
|
|
150
155
|
# Create Agent with explicit stream_fn from provider
|
|
151
156
|
from xdog.agent import AgentConfig
|
|
152
|
-
from xdog.agent.helpers import stream_fn_from_provider
|
|
157
|
+
from xdog.agent.helpers import model_supports_tool_calls, stream_fn_from_provider
|
|
153
158
|
from xdog.ai.types import StreamOptions
|
|
154
159
|
|
|
155
160
|
agent = Agent(
|
|
156
161
|
stream_fn_from_provider(provider),
|
|
157
162
|
config=AgentConfig(
|
|
158
163
|
model=model_id,
|
|
164
|
+
supports_tool_calls=model_supports_tool_calls(model_id),
|
|
159
165
|
system_prompt="", # will be rebuilt on first prompt
|
|
160
166
|
options=StreamOptions(
|
|
161
167
|
thinking=_as_thinking_level(thinking_level),
|
|
162
168
|
),
|
|
163
169
|
),
|
|
164
170
|
tools=agent_tools,
|
|
171
|
+
# The Agent owns where the index and the bodies go; this only says which
|
|
172
|
+
# manager to read them from.
|
|
173
|
+
skills=_coding_skill_manager(),
|
|
165
174
|
)
|
|
166
175
|
|
|
167
176
|
# Restore messages if resuming
|
|
@@ -183,3 +192,20 @@ def create_agent_session(options: CreateSessionOptions | None = None) -> CreateS
|
|
|
183
192
|
session=session,
|
|
184
193
|
model_fallback_message=model_fallback_message,
|
|
185
194
|
)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _coding_skill_manager() -> "SkillManager | None":
|
|
198
|
+
"""coding's SkillManager, or None if it cannot be built.
|
|
199
|
+
|
|
200
|
+
Skills are a convenience; failing to read them must not stop a session from
|
|
201
|
+
starting, which is why this swallows rather than raises.
|
|
202
|
+
"""
|
|
203
|
+
try:
|
|
204
|
+
from xdog.coding.core.slash_commands import skill_manager
|
|
205
|
+
|
|
206
|
+
return skill_manager()
|
|
207
|
+
except Exception: # pragma: no cover - defensive
|
|
208
|
+
import logging
|
|
209
|
+
|
|
210
|
+
logging.getLogger(__name__).debug("no skill manager available", exc_info=True)
|
|
211
|
+
return None
|
|
@@ -81,6 +81,7 @@ def build_system_prompt(
|
|
|
81
81
|
*,
|
|
82
82
|
file_entries: list[dict[str, Any]] | None = None,
|
|
83
83
|
extra_context: str = "",
|
|
84
|
+
native_tool_calls: bool = False,
|
|
84
85
|
) -> str:
|
|
85
86
|
"""Assemble the complete system prompt.
|
|
86
87
|
|
|
@@ -94,12 +95,23 @@ def build_system_prompt(
|
|
|
94
95
|
Optional processed file context entries.
|
|
95
96
|
extra_context:
|
|
96
97
|
Optional extra text appended at the end.
|
|
98
|
+
native_tool_calls:
|
|
99
|
+
True when the model receives tool definitions through the API, in which
|
|
100
|
+
case describing them again in the prompt sends every name, description
|
|
101
|
+
and parameter schema twice.
|
|
102
|
+
|
|
103
|
+
Defaults to False — *describe them* — because the two mistakes are not
|
|
104
|
+
symmetric. Describing tools to a model that already has them wastes
|
|
105
|
+
tokens in a cached prefix; omitting them for a model that has no other
|
|
106
|
+
way to see them leaves it unable to act, and that failure looks like the
|
|
107
|
+
model being useless rather than like a missing section.
|
|
97
108
|
"""
|
|
98
109
|
sections: list[str] = [
|
|
99
110
|
SYSTEM_PROMPT_HEADER,
|
|
100
111
|
build_environment_section(config),
|
|
101
|
-
build_tool_section(tools),
|
|
102
112
|
]
|
|
113
|
+
if not native_tool_calls:
|
|
114
|
+
sections.append(build_tool_section(tools))
|
|
103
115
|
|
|
104
116
|
if config.custom_instructions:
|
|
105
117
|
sections.append(
|
|
@@ -243,7 +243,15 @@ def test_parse_slash_command_splits_name_from_arguments() -> None:
|
|
|
243
243
|
def test_the_system_prompt_gains_and_loses_the_body(
|
|
244
244
|
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
245
245
|
) -> None:
|
|
246
|
-
"""Deactivation has to leave no trace, or `/unload` is theatre.
|
|
246
|
+
"""Deactivation has to leave no trace, or `/unload` is theatre.
|
|
247
|
+
|
|
248
|
+
Asserted against the Agent's system prompt rather than against the section
|
|
249
|
+
coding builds: the body is placed by `Agent.set_skills` now, so the section
|
|
250
|
+
is no longer where it lands. Checking what the model actually receives is
|
|
251
|
+
the better test anyway — it survives the placement moving again.
|
|
252
|
+
"""
|
|
253
|
+
from xdog.agent.agent import Agent
|
|
254
|
+
from xdog.agent.core import AgentConfig
|
|
247
255
|
from xdog.coding.core.agent_session import _skills_context
|
|
248
256
|
|
|
249
257
|
shared = tmp_path / "skills"
|
|
@@ -256,11 +264,21 @@ def test_the_system_prompt_gains_and_loses_the_body(
|
|
|
256
264
|
assert "write a workflow" in idle, "the one-line description is always advertised"
|
|
257
265
|
assert "STEP ONE" not in idle, "the body must wait to be asked for"
|
|
258
266
|
|
|
259
|
-
|
|
260
|
-
|
|
267
|
+
agent = Agent(
|
|
268
|
+
lambda *a, **k: None,
|
|
269
|
+
config=AgentConfig(model="m", system_prompt="BASE"),
|
|
270
|
+
skills=manager,
|
|
271
|
+
)
|
|
272
|
+
idle_prompt = agent.state.system_prompt
|
|
273
|
+
assert "write a workflow" in idle_prompt, "the index is always there"
|
|
274
|
+
assert "STEP ONE" not in idle_prompt, "the body waits to be asked for"
|
|
275
|
+
|
|
276
|
+
agent.set_active_skills(["flow"])
|
|
277
|
+
assert "STEP ONE: validate the graph." in agent.state.system_prompt
|
|
278
|
+
assert agent.state.system_prompt.strip().endswith("BASE"), "the caller's prompt survives"
|
|
261
279
|
|
|
262
|
-
|
|
263
|
-
assert
|
|
280
|
+
agent.set_active_skills([])
|
|
281
|
+
assert agent.state.system_prompt == idle_prompt, "byte for byte, or /unload is theatre"
|
|
264
282
|
|
|
265
283
|
|
|
266
284
|
def test_the_skills_section_is_empty_when_nothing_is_installed(
|
|
@@ -295,10 +313,15 @@ class FakeExpiringSession:
|
|
|
295
313
|
def __init__(self, active: set[str]) -> None:
|
|
296
314
|
self._active_skills = frozenset(active)
|
|
297
315
|
self.rebuilds = 0
|
|
316
|
+
self.pushed: list[list[str]] = []
|
|
298
317
|
|
|
299
318
|
def _rebuild_system_prompt(self) -> None:
|
|
300
319
|
self.rebuilds += 1
|
|
301
320
|
|
|
321
|
+
def _push_skills(self) -> None:
|
|
322
|
+
"""Expiry must tell the Agent too, or the body outlives the scope."""
|
|
323
|
+
self.pushed.append(sorted(self._active_skills))
|
|
324
|
+
|
|
302
325
|
|
|
303
326
|
def _expire(session: Any) -> None:
|
|
304
327
|
from xdog.coding.core.agent_session import AgentSession
|
|
@@ -387,3 +410,60 @@ def test_the_model_is_told_it_may_suggest_but_not_unload(
|
|
|
387
410
|
assert "cannot deactivate" in prompt
|
|
388
411
|
assert "/unload" in prompt
|
|
389
412
|
assert "the user decides" in prompt.lower()
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
# -- tool definitions were being sent twice ---------------------------------
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
def _cfg(model: str = "copilot/x"):
|
|
419
|
+
from xdog.coding.config import PlatformInfo, RuntimeConfig
|
|
420
|
+
|
|
421
|
+
return RuntimeConfig(
|
|
422
|
+
model=model, thinking_level="normal", allowed_tools=("read",),
|
|
423
|
+
custom_instructions="", extensions=(), working_dir="/tmp",
|
|
424
|
+
platform_info=PlatformInfo.detect(),
|
|
425
|
+
)
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
_TOOLS = [{"name": "read", "description": "Read a file", "parameters": {"path": "string"}}]
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
def test_a_native_tool_calling_model_is_not_told_its_tools_twice() -> None:
|
|
432
|
+
"""They already arrive as structured tool definitions through the API.
|
|
433
|
+
Describing them again sends every name, description and parameter schema a
|
|
434
|
+
second time, in the cached prefix, for a model that did not need it."""
|
|
435
|
+
from xdog.coding.core.system_prompt import build_system_prompt
|
|
436
|
+
|
|
437
|
+
described = build_system_prompt(_cfg(), _TOOLS, native_tool_calls=False)
|
|
438
|
+
native = build_system_prompt(_cfg(), _TOOLS, native_tool_calls=True)
|
|
439
|
+
|
|
440
|
+
assert "Read a file" in described
|
|
441
|
+
assert "Read a file" not in native
|
|
442
|
+
assert len(native) < len(described)
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
def test_the_default_keeps_tools_visible() -> None:
|
|
446
|
+
"""The two mistakes are not symmetric. A duplicate costs tokens; a model
|
|
447
|
+
that cannot see its tools cannot act, and that reads as the model being
|
|
448
|
+
useless rather than as a missing prompt section — so unknown means describe.
|
|
449
|
+
"""
|
|
450
|
+
from xdog.coding.core.system_prompt import build_system_prompt
|
|
451
|
+
|
|
452
|
+
assert "Read a file" in build_system_prompt(_cfg(), _TOOLS)
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
def test_an_unresolvable_model_still_gets_its_tools_described(
|
|
456
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
457
|
+
) -> None:
|
|
458
|
+
"""The lookup fails on an unknown provider, a network problem, or an offline
|
|
459
|
+
box. Every one of those must land on 'describe them'."""
|
|
460
|
+
from xdog.agent.helpers import model_supports_tool_calls
|
|
461
|
+
from xdog.coding.core.agent_session import AgentSession
|
|
462
|
+
|
|
463
|
+
class _Unresolvable:
|
|
464
|
+
class agent: # noqa: N801
|
|
465
|
+
class state: # noqa: N801
|
|
466
|
+
model = "no-such-provider/no-such-model"
|
|
467
|
+
|
|
468
|
+
assert model_supports_tool_calls("no-such-provider/no-such-model") is None
|
|
469
|
+
assert AgentSession._native_tool_calls(_Unresolvable()) is False
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{xdog_coding-2.0.1 → xdog_coding-2.1.0}/src/xdog/coding/modes/interactive/components/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{xdog_coding-2.0.1 → xdog_coding-2.1.0}/src/xdog/coding/modes/interactive/components/chat_log.py
RENAMED
|
File without changes
|
|
File without changes
|
{xdog_coding-2.0.1 → xdog_coding-2.1.0}/src/xdog/coding/modes/interactive/components/footer.py
RENAMED
|
File without changes
|
|
File without changes
|
{xdog_coding-2.0.1 → xdog_coding-2.1.0}/src/xdog/coding/modes/interactive/components/user_message.py
RENAMED
|
File without changes
|
{xdog_coding-2.0.1 → xdog_coding-2.1.0}/src/xdog/coding/modes/interactive/interactive_mode.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|