coderio 0.4.0__py3-none-any.whl
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.
- coderio/__init__.py +14 -0
- coderio/agent/__init__.py +5 -0
- coderio/agent/_deepagents_compat.py +139 -0
- coderio/agent/command_review.py +140 -0
- coderio/agent/deep_loop.py +758 -0
- coderio/agent/harness.py +768 -0
- coderio/agent/harness_middleware.py +293 -0
- coderio/agent/permission_middleware.py +52 -0
- coderio/agent/prompts.py +304 -0
- coderio/agent/skill_tool.py +70 -0
- coderio/agent/state.py +144 -0
- coderio/agent/stream.py +93 -0
- coderio/cli/__init__.py +0 -0
- coderio/cli/app.py +257 -0
- coderio/cli/commands.py +339 -0
- coderio/cli/credentials.py +70 -0
- coderio/cli/mcp_cmd.py +227 -0
- coderio/cli/multimodal.py +94 -0
- coderio/cli/onboarding.py +270 -0
- coderio/cli/providers.py +113 -0
- coderio/cli/render.py +28 -0
- coderio/cli/repl.py +189 -0
- coderio/cli/run_cmd.py +171 -0
- coderio/cli/skills_cmd.py +77 -0
- coderio/cli/stream.py +224 -0
- coderio/cli/tui.py +1584 -0
- coderio/cli/tui_onboarding.py +465 -0
- coderio/cli/tui_screens.py +334 -0
- coderio/cli/tui_widgets.py +453 -0
- coderio/config/__init__.py +21 -0
- coderio/config/bootstrap.py +38 -0
- coderio/config/loader.py +273 -0
- coderio/config/models.py +193 -0
- coderio/config/trust.py +136 -0
- coderio/llm/__init__.py +3 -0
- coderio/llm/factory.py +164 -0
- coderio/llm/probe.py +153 -0
- coderio/mcp_loader.py +274 -0
- coderio/session/__init__.py +3 -0
- coderio/session/message.py +79 -0
- coderio/session/store.py +244 -0
- coderio/skills/__init__.py +4 -0
- coderio/skills/bundled/executing-plans/SKILL.md +15 -0
- coderio/skills/lion-skills/LICENSE +21 -0
- coderio/skills/lion-skills/README.md +114 -0
- coderio/skills/lion-skills/skills/clarifying-questions/SKILL.md +119 -0
- coderio/skills/lion-skills/skills/clarifying-questions/evals/evals.json +40 -0
- coderio/skills/lion-skills/skills/code-review/SKILL.md +102 -0
- coderio/skills/lion-skills/skills/code-review/evals/evals.json +40 -0
- coderio/skills/lion-skills/skills/commit-message/SKILL.md +97 -0
- coderio/skills/lion-skills/skills/commit-message/evals/evals.json +20 -0
- coderio/skills/lion-skills/skills/debugging/SKILL.md +116 -0
- coderio/skills/lion-skills/skills/debugging/evals/evals.json +40 -0
- coderio/skills/lion-skills/skills/error-handling/SKILL.md +74 -0
- coderio/skills/lion-skills/skills/error-handling/evals/evals.json +20 -0
- coderio/skills/lion-skills/skills/lion-writing-skills/SKILL.md +85 -0
- coderio/skills/lion-skills/skills/lion-writing-skills/evals/evals.json +20 -0
- coderio/skills/lion-skills/skills/naming/SKILL.md +85 -0
- coderio/skills/lion-skills/skills/naming/evals/evals.json +20 -0
- coderio/skills/lion-skills/skills/onboarding-unknown-codebase/SKILL.md +95 -0
- coderio/skills/lion-skills/skills/onboarding-unknown-codebase/evals/evals.json +20 -0
- coderio/skills/lion-skills/skills/spec-writing/SKILL.md +154 -0
- coderio/skills/lion-skills/skills/spec-writing/evals/evals.json +40 -0
- coderio/skills/lion-skills/skills/task-breakdown/SKILL.md +134 -0
- coderio/skills/lion-skills/skills/task-breakdown/evals/evals.json +46 -0
- coderio/skills/lion-skills/skills/testing/SKILL.md +134 -0
- coderio/skills/lion-skills/skills/testing/evals/evals.json +40 -0
- coderio/skills/lion-skills/skills/verify-and-fix/SKILL.md +132 -0
- coderio/skills/lion-skills/skills/verify-and-fix/evals/evals.json +40 -0
- coderio/skills/models.py +66 -0
- coderio/skills/parser.py +50 -0
- coderio/skills/store.py +102 -0
- coderio/tools/__init__.py +94 -0
- coderio/tools/base.py +55 -0
- coderio/tools/bash.py +168 -0
- coderio/tools/command_policy.py +364 -0
- coderio/tools/edit_file.py +55 -0
- coderio/tools/glob_tool.py +21 -0
- coderio/tools/grep_tool.py +90 -0
- coderio/tools/linux_sandbox.py +196 -0
- coderio/tools/list_dir.py +58 -0
- coderio/tools/multi_edit.py +62 -0
- coderio/tools/note.py +72 -0
- coderio/tools/permission.py +214 -0
- coderio/tools/read_file.py +51 -0
- coderio/tools/sandbox_runner.py +113 -0
- coderio/tools/todo.py +71 -0
- coderio/tools/web_fetch.py +188 -0
- coderio/tools/web_search.py +49 -0
- coderio/tools/win_job.py +214 -0
- coderio/tools/win_sandbox.py +639 -0
- coderio/tools/write_file.py +25 -0
- coderio-0.4.0.dist-info/METADATA +490 -0
- coderio-0.4.0.dist-info/RECORD +97 -0
- coderio-0.4.0.dist-info/WHEEL +4 -0
- coderio-0.4.0.dist-info/entry_points.txt +2 -0
- coderio-0.4.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
"""Harness as a deepagents AgentMiddleware.
|
|
2
|
+
|
|
3
|
+
This ports coderio's structural harness (the "wrote code but never verified →
|
|
4
|
+
block done" hard constraint) into deepagents' middleware layer. The existing
|
|
5
|
+
Harness/HarnessState logic in agent/harness.py is reused unchanged; this module
|
|
6
|
+
is the adapter that wires it into deepagents' after_model / wrap_tool_call hooks.
|
|
7
|
+
|
|
8
|
+
Why this exists: deepagents is a batteries-included harness (planning tool,
|
|
9
|
+
filesystem, subagents, context management) but it does NOT enforce verification
|
|
10
|
+
before "done" — it trusts the agent. coderio's harness is the one structural
|
|
11
|
+
constraint that must survive the migration. As a middleware, it intercepts:
|
|
12
|
+
- wrap_tool_call: observe writes/executions (ground truth) + nudge (PlanGate)
|
|
13
|
+
- after_model: decide whether the model's "no tool_calls" (want-to-end) is
|
|
14
|
+
allowed, or force-continue via jump_to='model' (VerifyGate)
|
|
15
|
+
|
|
16
|
+
CRITICAL: after_model MUST be decorated with @hook_config(can_jump_to=["model"]).
|
|
17
|
+
Without it, langchain's factory sees can_jump_to=[] and does NOT build the
|
|
18
|
+
conditional edge for this middleware, so jump_to='model' silently does nothing —
|
|
19
|
+
the agent ends despite the harness wanting to force-continue. This was a latent
|
|
20
|
+
bug in the original version (verified against langchain factory.py
|
|
21
|
+
_get_can_jump_to, which reads method.__can_jump_to__).
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import re
|
|
27
|
+
from typing import Any
|
|
28
|
+
|
|
29
|
+
from langchain.agents.middleware.types import AgentMiddleware, hook_config
|
|
30
|
+
from langchain_core.messages import AIMessage, HumanMessage
|
|
31
|
+
|
|
32
|
+
from coderio.agent._deepagents_compat import get_state_todos
|
|
33
|
+
from coderio.agent.harness import Harness, HarnessState
|
|
34
|
+
from coderio.tools.todo import TodoStore
|
|
35
|
+
|
|
36
|
+
# deepagents tool-name mapping. deepagents uses 'execute' for shell (coderio
|
|
37
|
+
# used 'bash'), and write_file/edit_file (no multi_edit). We translate so the
|
|
38
|
+
# existing Harness logic works without modification.
|
|
39
|
+
_DEEP_VERIFY_TOOL = "execute"
|
|
40
|
+
_DEEP_WRITE_TOOLS = frozenset({"write_file", "edit_file"})
|
|
41
|
+
# deepagents' planning tool is 'write_todos' (coderio used 'todo'). The
|
|
42
|
+
# CompletionGate checks for pending todos — map deepagents' todo tool too.
|
|
43
|
+
_DEEP_TODO_TOOL = "write_todos"
|
|
44
|
+
|
|
45
|
+
# Translate "bash" → "execute" in harness injection prose. Word-boundary regex
|
|
46
|
+
# (robust to prose changes) instead of a chain of literal .replace() calls that
|
|
47
|
+
# silently no-op when harness.py rewords a phrase.
|
|
48
|
+
_BASH_TO_EXECUTE = re.compile(r"\bbash\b", re.IGNORECASE)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _stream_supports_phase(stream: Any) -> bool:
|
|
52
|
+
"""Does this stream consumer actually display phase changes?
|
|
53
|
+
|
|
54
|
+
Avoids paying AgentStateTracker overhead (and polluting the session jsonl
|
|
55
|
+
timeline) when no one is watching: NullStream (headless tests) and ad-hoc
|
|
56
|
+
stubs without on_phase_change return False. The real TUI StreamHandler and
|
|
57
|
+
the live-verify PrintStream opt in by defining on_phase_change.
|
|
58
|
+
"""
|
|
59
|
+
return stream is not None and hasattr(stream, "on_phase_change")
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _to_coderio_name(name: str) -> str:
|
|
63
|
+
"""Translate a deepagents tool name to the coderio name Harness expects."""
|
|
64
|
+
if name == _DEEP_VERIFY_TOOL:
|
|
65
|
+
return "bash"
|
|
66
|
+
if name == _DEEP_TODO_TOOL:
|
|
67
|
+
return "todo"
|
|
68
|
+
return name
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _result_to_text(result: Any) -> str:
|
|
72
|
+
"""Normalize a deepagents tool result (ToolMessage/str/object) to text for
|
|
73
|
+
the harness success/failure heuristic.
|
|
74
|
+
|
|
75
|
+
For ExecuteResponse (deepagents shell results), appends the exit_code as
|
|
76
|
+
``[exit_code: N]`` so the harness's VerifyGate can parse it. Without this,
|
|
77
|
+
a failed test run (exit != 0) would be treated as "verified" because the
|
|
78
|
+
raw output text doesn't contain the exit code marker.
|
|
79
|
+
"""
|
|
80
|
+
if isinstance(result, str):
|
|
81
|
+
return result
|
|
82
|
+
# deepagents ReadResult has an `error` attr for failed reads, and `file_data`
|
|
83
|
+
# for successful ones. Extract the error so the harness's not-found detection
|
|
84
|
+
# (harness.py: "not found" in result) works.
|
|
85
|
+
error = getattr(result, "error", None)
|
|
86
|
+
if isinstance(error, str) and error:
|
|
87
|
+
return error
|
|
88
|
+
# Build the text from content/output fields.
|
|
89
|
+
text = ""
|
|
90
|
+
content = getattr(result, "content", None)
|
|
91
|
+
if isinstance(content, str):
|
|
92
|
+
text = content
|
|
93
|
+
elif isinstance(content, list):
|
|
94
|
+
text = "".join(b.get("text", "") for b in content if isinstance(b, dict))
|
|
95
|
+
else:
|
|
96
|
+
output = getattr(result, "output", None)
|
|
97
|
+
if isinstance(output, str):
|
|
98
|
+
text = output
|
|
99
|
+
elif result is not None:
|
|
100
|
+
text = str(result)
|
|
101
|
+
# Append exit_code marker if the result has one (ExecuteResponse). This
|
|
102
|
+
# lets _parse_exit_code in harness.py extract it for VerifyGate.
|
|
103
|
+
exit_code = getattr(result, "exit_code", None)
|
|
104
|
+
if exit_code is not None and "[exit_code:" not in text:
|
|
105
|
+
text = f"{text}\n[exit_code: {exit_code}]"
|
|
106
|
+
return text
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class HarnessMiddleware(AgentMiddleware):
|
|
110
|
+
"""Enforces coderio's verification harness inside a deepagents agent loop.
|
|
111
|
+
|
|
112
|
+
Holds a Harness instance (with its own TodoStore). Observes every tool call
|
|
113
|
+
to track writes-since-verify, and intercepts the model's termination to block
|
|
114
|
+
"done" when code was written but never run (escalating: force-continue twice,
|
|
115
|
+
then release with a warning — never silent, never infinite).
|
|
116
|
+
|
|
117
|
+
Harness signals (force-continue / escalation-warn) are emitted via
|
|
118
|
+
runtime.stream_writer so the TUI's StreamHandler can show them. This is the
|
|
119
|
+
'custom' stream channel — distinct from 'messages' (token stream) and
|
|
120
|
+
'updates' (complete messages).
|
|
121
|
+
"""
|
|
122
|
+
|
|
123
|
+
def __init__(self, stream=None, enabled: bool = True) -> None:
|
|
124
|
+
# Wire the phase-observation tracker when a stream consumer is present.
|
|
125
|
+
# The display pipeline (TUI StatusBar / stream.on_phase_change) already
|
|
126
|
+
# exists; without a tracker, Harness._track_phase is a no-op and the
|
|
127
|
+
# status bar's phase slot stays empty. We instantiate the tracker
|
|
128
|
+
# whenever the stream declares on_phase_change support — that's the
|
|
129
|
+
# signal a real UI is listening (NullStream and test stubs don't).
|
|
130
|
+
from coderio.agent.state import AgentStateTracker
|
|
131
|
+
|
|
132
|
+
tracker = AgentStateTracker() if _stream_supports_phase(stream) else None
|
|
133
|
+
self.harness = Harness(
|
|
134
|
+
state=HarnessState(),
|
|
135
|
+
todos=TodoStore(),
|
|
136
|
+
enabled=enabled,
|
|
137
|
+
state_tracker=tracker,
|
|
138
|
+
stream=stream,
|
|
139
|
+
)
|
|
140
|
+
self.stream = stream
|
|
141
|
+
self._runtime = None # captured in wrap_tool_call / after_model
|
|
142
|
+
|
|
143
|
+
def _emit(self, runtime: Any, payload: dict) -> None:
|
|
144
|
+
"""Send a custom stream event (harness_continue / harness_warn).
|
|
145
|
+
|
|
146
|
+
Uses runtime.stream_writer so the stream consumer (deep_loop.py) picks
|
|
147
|
+
it up via stream_mode='custom'. Falls back to the legacy stream callback
|
|
148
|
+
if no runtime (e.g. in unit tests without a real graph).
|
|
149
|
+
"""
|
|
150
|
+
try:
|
|
151
|
+
if runtime is not None and hasattr(runtime, "stream_writer"):
|
|
152
|
+
runtime.stream_writer(payload)
|
|
153
|
+
return
|
|
154
|
+
except Exception: # noqa: S110 — stream_writer may not be available in tests; fall back
|
|
155
|
+
pass
|
|
156
|
+
# Fallback: direct stream callback (for tests / non-graph contexts).
|
|
157
|
+
if self.stream is not None:
|
|
158
|
+
t = payload.get("type")
|
|
159
|
+
if t == "harness_continue" and hasattr(self.stream, "on_harness_continue"):
|
|
160
|
+
self.stream.on_harness_continue(payload.get("reason", ""))
|
|
161
|
+
elif t == "harness_warn" and hasattr(self.stream, "on_harness_warn"):
|
|
162
|
+
self.stream.on_harness_warn(payload.get("message", ""))
|
|
163
|
+
|
|
164
|
+
# ------------------------------------------------------- observe tool calls
|
|
165
|
+
def wrap_tool_call(self, request, handler):
|
|
166
|
+
"""Observe every tool execution (ground truth) + apply PlanGate nudge.
|
|
167
|
+
|
|
168
|
+
Also captures the runtime reference for after_model's stream_writer use.
|
|
169
|
+
"""
|
|
170
|
+
self._runtime = getattr(request, "runtime", None) or self._runtime
|
|
171
|
+
tc = getattr(request, "tool_call", None) or {}
|
|
172
|
+
name = tc.get("name", "")
|
|
173
|
+
args = dict(tc.get("args", {}) or {})
|
|
174
|
+
# deepagents uses 'file_path' for write/edit tools; coderio's harness
|
|
175
|
+
# expects 'path'. Normalize so observe() records the right path.
|
|
176
|
+
if "file_path" in args and "path" not in args:
|
|
177
|
+
args["path"] = args["file_path"]
|
|
178
|
+
|
|
179
|
+
result = handler(request)
|
|
180
|
+
result_text = _result_to_text(result)
|
|
181
|
+
|
|
182
|
+
# Feed ground truth to the harness (translate deepagents → coderio names).
|
|
183
|
+
coderio_name = _to_coderio_name(name)
|
|
184
|
+
self.harness.observe(coderio_name, args, result_text)
|
|
185
|
+
|
|
186
|
+
# Sync deepagents' write_todos into the harness's TodoStore so
|
|
187
|
+
# CompletionGate can check for pending todos. Only sync when the tool
|
|
188
|
+
# actually succeeded — a failed write_todos (Error result) means the
|
|
189
|
+
# graph state wasn't updated, so syncing args would create a mismatch.
|
|
190
|
+
if name == "write_todos" and "todos" in args and not result_text.startswith("Error"):
|
|
191
|
+
from coderio.tools.todo import Todo
|
|
192
|
+
|
|
193
|
+
todos_data = args["todos"]
|
|
194
|
+
if isinstance(todos_data, list):
|
|
195
|
+
self.harness.todos.todos = [
|
|
196
|
+
Todo(content=t.get("content", ""), status=t.get("status", "pending"))
|
|
197
|
+
for t in todos_data
|
|
198
|
+
if isinstance(t, dict)
|
|
199
|
+
]
|
|
200
|
+
|
|
201
|
+
# Subagent delegation: the task tool returns a subagent's findings,
|
|
202
|
+
# which may cite files the subagent read but the MAIN agent didn't.
|
|
203
|
+
# Without this, GroundingGate would flag those citations as ungrounded
|
|
204
|
+
# and force-continue — turning a complete analysis into a "correction".
|
|
205
|
+
# We extract file paths from the subagent's result and add them to the
|
|
206
|
+
# main agent's read set, so the gate treats them as "read via subagent".
|
|
207
|
+
if name == "task" and result_text:
|
|
208
|
+
from coderio.agent.harness import _cited_files, _norm_path
|
|
209
|
+
|
|
210
|
+
for cited in _cited_files(result_text):
|
|
211
|
+
# Strip :line suffix, normalize.
|
|
212
|
+
clean = cited.rsplit(":", 1)[0] if ":" in cited else cited
|
|
213
|
+
self.harness.state.content_read_files.add(_norm_path(clean))
|
|
214
|
+
|
|
215
|
+
# PlanGate: nudge if writing without a todo list (soft, appends to result).
|
|
216
|
+
aug = self.harness.after_tool_call(coderio_name, args, result_text)
|
|
217
|
+
if aug and isinstance(result, str):
|
|
218
|
+
result = result + aug
|
|
219
|
+
elif aug:
|
|
220
|
+
# result is a ToolMessage-like object; append to its content if possible
|
|
221
|
+
try:
|
|
222
|
+
result.content = _result_to_text(result) + aug
|
|
223
|
+
except (AttributeError, TypeError):
|
|
224
|
+
pass
|
|
225
|
+
return result
|
|
226
|
+
|
|
227
|
+
# ------------------------------------------------------- intercept termination
|
|
228
|
+
@hook_config(can_jump_to=["model"])
|
|
229
|
+
def after_model(self, state, runtime):
|
|
230
|
+
"""The model produced output. If it wants to end (no tool_calls) but the
|
|
231
|
+
harness says verification is missing, force the loop to continue.
|
|
232
|
+
|
|
233
|
+
Returns a state update dict: {'jump_to':'model','messages':[...]} to
|
|
234
|
+
force-continue, or None to let the agent end normally. On escalation
|
|
235
|
+
release, emits a harness_warn signal via stream_writer.
|
|
236
|
+
|
|
237
|
+
The @hook_config(can_jump_to=["model"]) decorator is REQUIRED — without
|
|
238
|
+
it, langchain's factory doesn't build the conditional edge and jump_to
|
|
239
|
+
silently fails (the agent ends anyway).
|
|
240
|
+
"""
|
|
241
|
+
self._runtime = runtime or self._runtime
|
|
242
|
+
messages = state.get("messages", []) if hasattr(state, "get") else getattr(state, "messages", [])
|
|
243
|
+
last = messages[-1] if messages else None
|
|
244
|
+
# Only intercept when the model returned final text (no tool calls).
|
|
245
|
+
if not isinstance(last, AIMessage) or getattr(last, "tool_calls", None):
|
|
246
|
+
return None
|
|
247
|
+
|
|
248
|
+
# Sync todos from graph state before checking termination. This covers
|
|
249
|
+
# the case where write_todos was called in a PREVIOUS turn (restored
|
|
250
|
+
# from checkpoint) but the HarnessMiddleware's TodoStore was reset (it's
|
|
251
|
+
# recreated each run_deep_agent call). Without this, CompletionGate
|
|
252
|
+
# would see an empty todo list even though graph state has pending todos.
|
|
253
|
+
state_todos = get_state_todos(state)
|
|
254
|
+
if state_todos and isinstance(state_todos, list):
|
|
255
|
+
from coderio.tools.todo import Todo
|
|
256
|
+
|
|
257
|
+
self.harness.todos.todos = [
|
|
258
|
+
Todo(content=t.get("content", ""), status=t.get("status", "pending"))
|
|
259
|
+
for t in state_todos
|
|
260
|
+
if isinstance(t, dict)
|
|
261
|
+
]
|
|
262
|
+
|
|
263
|
+
text = ""
|
|
264
|
+
content = getattr(last, "content", "")
|
|
265
|
+
if isinstance(content, str):
|
|
266
|
+
text = content
|
|
267
|
+
elif isinstance(content, list):
|
|
268
|
+
text = "".join(b.get("text", "") for b in content if isinstance(b, dict))
|
|
269
|
+
|
|
270
|
+
cont, inject, warn = self.harness.check_termination(text)
|
|
271
|
+
if cont and inject:
|
|
272
|
+
# The shared harness.py writes tool-name-agnostic prose that happens
|
|
273
|
+
# to say "bash" (its original engine's name for the shell tool).
|
|
274
|
+
# deepagents' shell tool is named 'execute'. Translate the standalone
|
|
275
|
+
# word "bash" → "execute" so the model calls the right tool.
|
|
276
|
+
# Word-boundary regex (not literal phrase replace) is robust to
|
|
277
|
+
# harness.py prose changes: "use bash", "call bash now", "run with
|
|
278
|
+
# bash" all map correctly without a per-phrase .replace() chain.
|
|
279
|
+
inject = _BASH_TO_EXECUTE.sub("execute", inject)
|
|
280
|
+
# Emit a visible signal so the TUI explains why the agent keeps running.
|
|
281
|
+
self._emit(runtime, {"type": "harness_continue", "reason": inject})
|
|
282
|
+
# Force-continue: inject the harness demand as a user message.
|
|
283
|
+
return {"jump_to": "model", "messages": [HumanMessage(content=inject)]}
|
|
284
|
+
if warn:
|
|
285
|
+
self._emit(runtime, {"type": "harness_warn", "message": warn})
|
|
286
|
+
# Fire the final phase transition (COMPLETE) when the turn is truly
|
|
287
|
+
# ending — either a clean finish or an escalation release. The TUI's
|
|
288
|
+
# on_phase_change clears the status bar's phase slot on 'complete'.
|
|
289
|
+
if self.harness.state_tracker is not None:
|
|
290
|
+
self.harness.state_tracker.finish(hint="turn end")
|
|
291
|
+
if self.stream is not None and hasattr(self.stream, "on_phase_change"):
|
|
292
|
+
self.stream.on_phase_change("complete", 0, "turn end")
|
|
293
|
+
return None
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Permission gate as a deepagents AgentMiddleware.
|
|
2
|
+
|
|
3
|
+
Wraps coderio's PermissionGate (the 4-tier access system: plan/confirm/
|
|
4
|
+
auto_edit/full) so it runs BEFORE every tool execution inside a deepagents
|
|
5
|
+
agent loop.
|
|
6
|
+
|
|
7
|
+
No path translation or WorkspacePolicy — file path isolation is handled by
|
|
8
|
+
deepagents' backend virtual_mode (/foo.py → {root}/foo.py, forced
|
|
9
|
+
relative_to(root) check). This middleware only controls WHICH tool types
|
|
10
|
+
may execute based on the permission mode.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from langchain.agents.middleware.types import AgentMiddleware
|
|
16
|
+
from langchain_core.messages import ToolMessage
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class PermissionMiddleware(AgentMiddleware):
|
|
20
|
+
"""Enforces coderio's permission gate inside a deepagents agent loop.
|
|
21
|
+
|
|
22
|
+
Intercepts every tool call via wrap_tool_call. Calls gate.check() with
|
|
23
|
+
the tool name + args BEFORE execution. The gate decides based on tool
|
|
24
|
+
type + permission mode (FULL/CONFIRM/AUTO_EDIT/PLAN).
|
|
25
|
+
|
|
26
|
+
Tool names are passed as-is (deepagents native names like 'execute',
|
|
27
|
+
'write_file', 'edit_file'). The tool classification sets in base.py
|
|
28
|
+
use these same native names.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
def __init__(self, gate) -> None:
|
|
32
|
+
self.gate = gate
|
|
33
|
+
|
|
34
|
+
def wrap_tool_call(self, request, handler):
|
|
35
|
+
"""Check permission before executing; block if denied."""
|
|
36
|
+
tc = getattr(request, "tool_call", None) or {}
|
|
37
|
+
name = tc.get("name", "")
|
|
38
|
+
args = dict(tc.get("args", {}) or {})
|
|
39
|
+
|
|
40
|
+
decision = self.gate.check(name, args)
|
|
41
|
+
if decision is True:
|
|
42
|
+
return handler(request)
|
|
43
|
+
|
|
44
|
+
# Denied (False) or custom reply (str). Build a ToolMessage so the model
|
|
45
|
+
# gets a clear, actionable result it can react to.
|
|
46
|
+
tool_call_id = tc.get("id", "")
|
|
47
|
+
if decision is False:
|
|
48
|
+
content = f"Permission denied: tool {name!r} blocked in {self.gate.mode} mode."
|
|
49
|
+
else:
|
|
50
|
+
# str — user's custom reply. Deny execution but feed the instruction.
|
|
51
|
+
content = f"Permission denied by user: {decision}"
|
|
52
|
+
return ToolMessage(content=content, tool_call_id=tool_call_id, name=name)
|
coderio/agent/prompts.py
ADDED
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
|
|
5
|
+
from coderio.skills.store import SkillStore
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass
|
|
9
|
+
class ActiveSkills:
|
|
10
|
+
"""Tracks currently-active skills for the session (spec §2.4)."""
|
|
11
|
+
|
|
12
|
+
_active: dict = field(default_factory=dict) # name -> Skill
|
|
13
|
+
|
|
14
|
+
def activate(self, skill) -> bool:
|
|
15
|
+
"""Activate a skill (load its body into context). Idempotent: if the
|
|
16
|
+
skill is already active, does nothing and returns False (avoids
|
|
17
|
+
re-reading the file on duplicate activation). Returns True if newly activated."""
|
|
18
|
+
if skill is None:
|
|
19
|
+
return False
|
|
20
|
+
if skill.name in self._active:
|
|
21
|
+
return False # already active — skip the redundant load_body()
|
|
22
|
+
skill.load_body()
|
|
23
|
+
self._active[skill.name] = skill
|
|
24
|
+
return True
|
|
25
|
+
|
|
26
|
+
def deactivate(self, name: str) -> bool:
|
|
27
|
+
return self._active.pop(name, None) is not None
|
|
28
|
+
|
|
29
|
+
def is_active(self, name: str) -> bool:
|
|
30
|
+
return name in self._active
|
|
31
|
+
|
|
32
|
+
def all(self) -> list:
|
|
33
|
+
return list(self._active.values())
|
|
34
|
+
|
|
35
|
+
def clear(self) -> None:
|
|
36
|
+
self._active.clear()
|
|
37
|
+
|
|
38
|
+
def active_tools(self) -> list:
|
|
39
|
+
"""Aggregate executable tools carried by all active skills.
|
|
40
|
+
|
|
41
|
+
Each skill may ship a tools.py with a TOOLS list (see Skill.load_tools).
|
|
42
|
+
When a skill is activated, its tools are added here so the agent loop
|
|
43
|
+
can bind them to the model. Deactivation removes them (naturally — the
|
|
44
|
+
skill is dropped from _active). Deduplicates by tool name (first wins).
|
|
45
|
+
"""
|
|
46
|
+
tools: list = []
|
|
47
|
+
seen: set[str] = set()
|
|
48
|
+
for skill in self._active.values():
|
|
49
|
+
for t in skill.load_tools():
|
|
50
|
+
t_name = getattr(t, "name", "")
|
|
51
|
+
if t_name and t_name not in seen:
|
|
52
|
+
tools.append(t)
|
|
53
|
+
seen.add(t_name)
|
|
54
|
+
return tools
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _os_context() -> str:
|
|
58
|
+
"""Build an OS-specific context clause for the system prompt.
|
|
59
|
+
|
|
60
|
+
The bash tool's behavior differs by platform (Git Bash on Windows, native
|
|
61
|
+
bash on Linux/macOS). Telling the model the wrong OS causes it to emit
|
|
62
|
+
wrong path styles or platform-specific idioms."""
|
|
63
|
+
import sys
|
|
64
|
+
|
|
65
|
+
if sys.platform == "win32":
|
|
66
|
+
return (
|
|
67
|
+
"You are coderio, a coding agent working in the user's project on Windows.\n"
|
|
68
|
+
"You have tools to read/write/edit files, run bash commands (Git Bash), search, manage"
|
|
69
|
+
)
|
|
70
|
+
return (
|
|
71
|
+
"You are coderio, a coding agent working in the user's project.\n"
|
|
72
|
+
"You have tools to read/write/edit files, run bash commands, search, manage"
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
_BASE_INSTRUCTIONS = """\
|
|
77
|
+
{os_context}
|
|
78
|
+
todos, and fetch the web. You serve TWO kinds of requests — recognize which one and behave
|
|
79
|
+
accordingly. Getting this wrong (running the heavy coding workflow on a quick question, or
|
|
80
|
+
answering a coding task with chat alone) is a failure mode.
|
|
81
|
+
|
|
82
|
+
## 0. Core behavior
|
|
83
|
+
|
|
84
|
+
• Be concise and direct. Don't over-explain unless asked. Lead with the answer or action,
|
|
85
|
+
not with "I'll now..." or "Let me..." — just do it.
|
|
86
|
+
• Professional objectivity: prioritize accuracy over validating the user's beliefs.
|
|
87
|
+
Disagree respectfully when the user is incorrect. Avoid unnecessary superlatives,
|
|
88
|
+
praise, or emotional validation.
|
|
89
|
+
• When something fails repeatedly, STOP and analyze WHY — don't keep retrying the same
|
|
90
|
+
approach. If you're blocked, tell the user what's wrong and ask for guidance.
|
|
91
|
+
• Keep working until the task is fully complete. Don't stop partway and describe what you
|
|
92
|
+
would do — just do it. Only yield back when done or genuinely blocked.
|
|
93
|
+
• For longer tasks, give brief progress updates at reasonable intervals (one sentence
|
|
94
|
+
recapping what's done and what's next). Don't narrate every tool call.
|
|
95
|
+
• Prefer small, verifiable changes over large rewrites. Make a change, test it, then
|
|
96
|
+
move on. If a change turns out wrong, revert and try a different approach — don't
|
|
97
|
+
stack patches on a broken foundation.
|
|
98
|
+
|
|
99
|
+
## 1. First, classify the intent
|
|
100
|
+
|
|
101
|
+
Every user message is ONE of these. Decide before acting:
|
|
102
|
+
|
|
103
|
+
• CODE — the user wants code written/changed/created/deleted, a bug fixed, a feature
|
|
104
|
+
built, a file refactored, a command run that mutates state. Signals:
|
|
105
|
+
"实现/写/改/修/加/重构/跑一下/构建/部署" (CN) or
|
|
106
|
+
"implement/write/fix/modify/add/refactor/build/deploy/create" (EN) —
|
|
107
|
+
any request that should end with files or commands changing.
|
|
108
|
+
• QA — the user wants knowledge or an explanation:
|
|
109
|
+
"X 是什么 / 为什么 / 怎么理解 / 有什么区别 / 对比一下 / 解释一下" (CN) or
|
|
110
|
+
"what is / why / how does / explain / difference between / compare" (EN).
|
|
111
|
+
No file should change. You MAY use read-only tools
|
|
112
|
+
(list_dir/read_file/grep/web_search/web_fetch) to ground your answer, but you must NOT
|
|
113
|
+
write/edit files or build a todo list.
|
|
114
|
+
• ANALYZE — the user wants a judgment or assessment of existing code/design/architecture:
|
|
115
|
+
"这样设计好不好 / 哪个方案更合适 / 帮我评审 / 这个实现有什么问题 / 怎么优化" (CN) or
|
|
116
|
+
"is this design good / which approach is better / review this / what's wrong with /
|
|
117
|
+
how to improve / assess" (EN). You MUST
|
|
118
|
+
read the relevant code first, then answer with EVIDENCE (cite what you read), separate
|
|
119
|
+
fact from speculation, and present trade-offs rather than dogmatic conclusions.
|
|
120
|
+
|
|
121
|
+
If a message mixes intents (e.g. "explain X and then implement it"), handle the QA/ANALYZE
|
|
122
|
+
part conversationally FIRST, then switch to CODE mode for the implementation.
|
|
123
|
+
|
|
124
|
+
## 2. CODE mode — the coding workflow (MANDATORY when writing code)
|
|
125
|
+
|
|
126
|
+
For ANY code task, follow this workflow. Each step has a corresponding skill playbook —
|
|
127
|
+
the step summaries below are ALWAYS in effect (they are your skeleton); the FULL playbook
|
|
128
|
+
for a step is loaded on-demand via activate_skill(step_name) when you reach that step and
|
|
129
|
+
need its detailed guidance. Do not load a playbook unless you are at that step.
|
|
130
|
+
|
|
131
|
+
0. EXPLORE FIRST — before proposing anything, use list_dir / read_file / grep to look at
|
|
132
|
+
the current directory and relevant files. Build an accurate picture of the project
|
|
133
|
+
(structure, conventions, what already exists) BEFORE you ask questions or write code.
|
|
134
|
+
NEVER generate code based on assumptions about files you haven't read. If the task
|
|
135
|
+
touches existing code, read that code first.
|
|
136
|
+
1. clarifying-questions — AFTER exploring, if the request has ANY ambiguity (tech stack
|
|
137
|
+
unspecified, scope unclear, hidden assumptions, possible X-Y problem), ask clarifying
|
|
138
|
+
questions FIRST and WAIT for the user's answers.
|
|
139
|
+
2. spec-writing — for non-trivial features, write a short design/spec.
|
|
140
|
+
3. task-breakdown — break the work into verifiable, ordered tasks (use the todo tool).
|
|
141
|
+
Batch all edits for one task before updating the todo list — don't call
|
|
142
|
+
write_todos after every single edit. Update todos when a task STATUS
|
|
143
|
+
changes (pending → in_progress → completed), not after each file edit.
|
|
144
|
+
Each write_todos call costs a full model round-trip, so updating 5 times
|
|
145
|
+
for 5 small edits wastes 4 unnecessary calls.
|
|
146
|
+
4. executing-plans — implement task-by-task, verify each step, commit frequently.
|
|
147
|
+
Read a file ONCE per task — don't re-read it after every edit. You already
|
|
148
|
+
know its contents from the first read; trust your memory within the same
|
|
149
|
+
task. Re-read only if you're switching to a DIFFERENT task or if edits by
|
|
150
|
+
another tool may have changed lines you need.
|
|
151
|
+
EXECUTION STAGE — while/after implementing, bring in the right skill (call
|
|
152
|
+
activate_skill to load one when it matches):
|
|
153
|
+
• testing — turn the task's "完成定义" into a repeatable test.
|
|
154
|
+
• debugging — when a bug/error/crash appears, locate the ROOT CAUSE first
|
|
155
|
+
(observe → hypothesize → experiment → narrow), THEN fix — don't guess-edit.
|
|
156
|
+
• code-review — before committing, review correctness/security first, then
|
|
157
|
+
readability (leave style to linters). Issues found flow into verify-and-fix.
|
|
158
|
+
5. verify-and-fix — turn "done" into "verified done"; fix root causes not symptoms.
|
|
159
|
+
(The harness will BLOCK you from declaring done if you wrote code but never ran it.)
|
|
160
|
+
6. commit-message — write proper commit messages when committing.
|
|
161
|
+
|
|
162
|
+
Only a truly trivial, single-action CODE task (e.g. "fix this one typo", "rename this
|
|
163
|
+
variable") may skip clarification. If you are about to CREATE a file or WRITE multiple
|
|
164
|
+
lines of code, the task is NOT trivial — clarify first. When unsure whether to clarify,
|
|
165
|
+
ALWAYS clarify. It is better to ask one question than to build the wrong thing.
|
|
166
|
+
|
|
167
|
+
### Large files (avoid truncation)
|
|
168
|
+
Your single response has a token limit (~16k). A large file (e.g. a 500-line game in one
|
|
169
|
+
HTML file) will be TRUNCATED if written in one write_file call, producing a broken file.
|
|
170
|
+
For large files: write the skeleton first (write_file with the structure/stubs), then fill
|
|
171
|
+
in sections with edit_file/multi_edit or note append. Prefer multiple smaller files over one
|
|
172
|
+
giant file. If a file needs >300 lines, split the work across several tool calls.
|
|
173
|
+
|
|
174
|
+
## 3. QA / ANALYZE mode — the general-agent guarantees
|
|
175
|
+
|
|
176
|
+
When the intent is NOT code-writing, you are a capable general assistant. These guarantees
|
|
177
|
+
apply (they are what make you a usable agent, not just a code-spitter):
|
|
178
|
+
|
|
179
|
+
• **Match effort to the question.** A greeting ("你好"/"hello"), small talk, or a simple
|
|
180
|
+
factual question gets a direct text reply — NO tools, NO exploration, NO "let me first
|
|
181
|
+
look at the project." Only reach for tools when the user's question actually requires
|
|
182
|
+
reading files or running commands. If you can answer from your own knowledge, do so.
|
|
183
|
+
• Be direct and concise. Answer the actual question in the user's language (Chinese in,
|
|
184
|
+
Chinese out). Do not pad, do not preface, do not apologize. Lead with the answer.
|
|
185
|
+
• Ground answers in the project. If the question is about THIS codebase, READ the relevant
|
|
186
|
+
files first (read_file/grep) — never explain "your" code from assumption. Quote the
|
|
187
|
+
specific lines/functions you base your answer on.
|
|
188
|
+
• Separate fact from speculation. State what you verified by reading vs. what you infer.
|
|
189
|
+
For ANALYZE especially: present trade-offs and alternatives, not a single dogmatic take.
|
|
190
|
+
"It depends on X; if Y then A is better, if Z then B" beats "always do A".
|
|
191
|
+
• **Never cite a file you haven't read this turn.** A claim like "loader.py:81 does X"
|
|
192
|
+
is only valid if you opened loader.py. Documentation and memory go stale; the source
|
|
193
|
+
is ground truth. If you find yourself about to assert something about a file's
|
|
194
|
+
contents, read it FIRST — otherwise you risk confidently stating things that are
|
|
195
|
+
wrong. (This is enforced structurally: the harness blocks ungrounded citations.
|
|
196
|
+
Don't fight it — read the file and ground your analysis.)
|
|
197
|
+
• Use tools to be accurate, not to perform. web_search/web_fetch when the user asks about
|
|
198
|
+
something external; read_file/grep when about the repo. But don't tool-call for show on a
|
|
199
|
+
question you can answer directly.
|
|
200
|
+
• **Activate the matching skill for non-trivial ANALYZE tasks.** Skills are your playbooks —
|
|
201
|
+
loading one gives you a structured methodology instead of freestyling. Trigger rules:
|
|
202
|
+
- **First contact with a project in a new conversation** (no prior context about its
|
|
203
|
+
structure, tech stack, or conventions): activate_skill("onboarding-unknown-codebase")
|
|
204
|
+
as your VERY FIRST action, before any list_dir/read_file. This is mandatory — you
|
|
205
|
+
cannot meaningfully analyze or review a codebase you haven't systematically mapped.
|
|
206
|
+
"分析这个项目" / "看看这个目录" / "有哪些值得优化的点" (CN) or
|
|
207
|
+
"analyze this project" / "look at this directory" / "what can be improved" (EN)
|
|
208
|
+
in a fresh conversation ALL qualify as first-contact → start with onboarding.
|
|
209
|
+
- **Already familiar with the project** (this conversation has established context),
|
|
210
|
+
user asks for quality assessment: activate_skill("code-review").
|
|
211
|
+
- "这段代码有什么 bug/怎么调试" (CN) / "what's the bug / how to debug" (EN)
|
|
212
|
+
→ activate_skill("debugging").
|
|
213
|
+
- "这段代码怎么改进/有什么问题" (CN) / "how to improve / what's wrong" (EN)
|
|
214
|
+
(project already understood) → activate_skill("code-review").
|
|
215
|
+
Do this BEFORE you start analyzing. Simple factual questions ("这个函数返回什么") don't
|
|
216
|
+
need a skill — they're answered by reading one file.
|
|
217
|
+
• One unified clarification principle across BOTH modes: when genuinely unsure what the
|
|
218
|
+
user needs, ask ONE focused question rather than guessing. In QA/ANALYZE this is light
|
|
219
|
+
(a quick clarifying reply); in CODE it is the structured clarifying-questions skill.
|
|
220
|
+
|
|
221
|
+
## 4. Tools (reference)
|
|
222
|
+
|
|
223
|
+
read_file / ls / glob / grep — read the project (always allowed)
|
|
224
|
+
write_file / edit_file — mutate files (CODE mode; permission-gated)
|
|
225
|
+
execute — run shell commands, verify code (CODE mode; permission-gated)
|
|
226
|
+
write_todos — manage a task list (CODE mode, for non-trivial work)
|
|
227
|
+
web_search / web_fetch — external info (any mode)
|
|
228
|
+
activate_skill(name) — load an optional playbook (naming, debugging, error-handling,
|
|
229
|
+
onboarding-unknown-codebase, etc.) when a task matches it.
|
|
230
|
+
|
|
231
|
+
### Path conventions (OVERRIDES the framework's default examples)
|
|
232
|
+
|
|
233
|
+
The framework's built-in prompt may show macOS-style paths like `/Users/name/...` —
|
|
234
|
+
IGNORE those examples. This project uses **virtual paths** rooted at `/`:
|
|
235
|
+
- `/AGENTS.md`, `/src/coderio/agent/deep_loop.py`, `/pyproject.toml` — always leading `/`
|
|
236
|
+
- NEVER use Windows paths (C:\\... or E:\\...) or macOS paths (/Users/...) with read_file/ls/etc.
|
|
237
|
+
- If a path doesn't exist, it's not a system error — the file just isn't there.
|
|
238
|
+
|
|
239
|
+
**IMPORTANT: execute (shell) paths differ from file-tool paths.**
|
|
240
|
+
- File tools (read_file, write_file, edit_file, ls, glob, grep) use virtual paths
|
|
241
|
+
like `/foo.py` — the leading `/` maps to the project root.
|
|
242
|
+
- The `execute` tool runs REAL shell commands. Virtual paths like `/foo.py` do NOT
|
|
243
|
+
work in shell — `python /foo.py` will fail because the OS interprets `/foo.py`
|
|
244
|
+
as an absolute OS path (e.g. `C:\foo.py` on Windows), not the project root.
|
|
245
|
+
- In `execute`, use RELATIVE paths instead: `python foo.py`, `cat foo.py`, `ls -la`.
|
|
246
|
+
- The shell's working directory is already the project root — relative paths work
|
|
247
|
+
correctly without any prefix.
|
|
248
|
+
|
|
249
|
+
### Tool failure recovery (CRITICAL)
|
|
250
|
+
|
|
251
|
+
When a tool call fails, DO NOT immediately conclude "the environment is broken" or
|
|
252
|
+
"the file doesn't exist". Investigate using other tools first:
|
|
253
|
+
|
|
254
|
+
• read_file returns "file not found" → the path is likely wrong (relative paths resolve
|
|
255
|
+
from the project root, not the current file's directory). Use glob("**/filename") or
|
|
256
|
+
grep to find the actual location, then retry with the correct path.
|
|
257
|
+
• bash `python -m pytest` fails with "No module named X" → the `python` on PATH may be
|
|
258
|
+
the wrong interpreter (e.g. Python 2.7 on Windows). Check `.venv/Scripts/python.exe`
|
|
259
|
+
(Windows) or `venv/bin/python` (Linux/macOS) — if the project has a virtual environment,
|
|
260
|
+
use that interpreter directly. Run `which -a python python3` to see what's available.
|
|
261
|
+
• bash command not found → some tools differ between platforms (rg vs grep, py vs python3).
|
|
262
|
+
Use `which <tool>` to check availability before assuming it exists.
|
|
263
|
+
• Any tool error → retry with a corrected approach. The first failure is a signal to
|
|
264
|
+
investigate, not a conclusion. The harness will not penalize you for extra discovery
|
|
265
|
+
calls, but it WILL penalize you for claiming "done" based on a failed verification.
|
|
266
|
+
"""
|
|
267
|
+
|
|
268
|
+
# The core workflow chain — these are injected as runtime rules by default, NOT
|
|
269
|
+
# left to the LLM to optionally activate. Cross-cutting skills remain opt-in.
|
|
270
|
+
CORE_CHAIN_SKILLS = (
|
|
271
|
+
"clarifying-questions",
|
|
272
|
+
"spec-writing",
|
|
273
|
+
"task-breakdown",
|
|
274
|
+
"executing-plans",
|
|
275
|
+
"verify-and-fix",
|
|
276
|
+
"commit-message",
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def build_system_prompt(store: SkillStore, active: ActiveSkills) -> str:
|
|
281
|
+
parts = [_BASE_INSTRUCTIONS.format(os_context=_os_context())]
|
|
282
|
+
|
|
283
|
+
# All skills (core chain + cross-cutting) are listed by DESCRIPTION only — the
|
|
284
|
+
# full playbook body is loaded on-demand via activate_skill() when the model
|
|
285
|
+
# judges it relevant. This is the "progressive disclosure" pattern (matches
|
|
286
|
+
# Anthropic Agent Skills / Claude Code): keep the system prompt small (~tens of
|
|
287
|
+
# descriptions, not thousands of chars of body text) and load bodies lazily.
|
|
288
|
+
# Previously CORE_CHAIN_SKILLS bodies were all injected at once (~20K chars /
|
|
289
|
+
# 75% of the prompt) regardless of relevance — slow, costly, attention-diluting.
|
|
290
|
+
descs = store.descriptions_for_prompt()
|
|
291
|
+
if descs:
|
|
292
|
+
parts.append(
|
|
293
|
+
"Available skills — ACTIVATE the matching one before starting non-trivial work "
|
|
294
|
+
"(call activate_skill(name) to load its full playbook). Skills give you a "
|
|
295
|
+
"structured methodology; without one you're freestyling and likely missing "
|
|
296
|
+
"systemic issues. Match by the task, not by guessing:\n" + descs
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
# Explicitly user/model-activated skills: their bodies ARE injected (the model
|
|
300
|
+
# asked for them, so they're now in context).
|
|
301
|
+
active_bodies = [s.body for s in active.all()]
|
|
302
|
+
if active_bodies:
|
|
303
|
+
parts.append("Active skill playbooks (loaded into context):\n\n" + "\n\n---\n\n".join(active_bodies))
|
|
304
|
+
return "\n\n".join(parts)
|