power-loop 0.2.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.
- llm_client/__init__.py +0 -0
- llm_client/capabilities.py +162 -0
- llm_client/interface.py +470 -0
- llm_client/llm_factory.py +981 -0
- llm_client/llm_tooling.py +645 -0
- llm_client/llm_utils.py +205 -0
- llm_client/multimodal.py +237 -0
- llm_client/qwen_image.py +576 -0
- llm_client/web_search.py +149 -0
- power_loop/__init__.py +326 -0
- power_loop/agent/__init__.py +6 -0
- power_loop/agent/sink.py +247 -0
- power_loop/agent/stateful_loop.py +363 -0
- power_loop/agent/system_prompt.py +396 -0
- power_loop/agent/types.py +41 -0
- power_loop/contracts/__init__.py +132 -0
- power_loop/contracts/errors.py +140 -0
- power_loop/contracts/event_payloads.py +278 -0
- power_loop/contracts/events.py +86 -0
- power_loop/contracts/handlers.py +45 -0
- power_loop/contracts/hook_contexts.py +265 -0
- power_loop/contracts/hooks.py +64 -0
- power_loop/contracts/messages.py +90 -0
- power_loop/contracts/protocols.py +48 -0
- power_loop/contracts/tools.py +56 -0
- power_loop/core/agent_context.py +94 -0
- power_loop/core/events.py +124 -0
- power_loop/core/hooks.py +122 -0
- power_loop/core/phase.py +217 -0
- power_loop/core/pipeline.py +880 -0
- power_loop/core/runner.py +60 -0
- power_loop/core/state.py +208 -0
- power_loop/runtime/budget.py +179 -0
- power_loop/runtime/cancellation.py +127 -0
- power_loop/runtime/compact.py +300 -0
- power_loop/runtime/env.py +103 -0
- power_loop/runtime/memory.py +107 -0
- power_loop/runtime/provider.py +176 -0
- power_loop/runtime/retry.py +182 -0
- power_loop/runtime/session_store.py +636 -0
- power_loop/runtime/skills.py +201 -0
- power_loop/runtime/spec.py +233 -0
- power_loop/runtime/structured.py +225 -0
- power_loop/tools/__init__.py +51 -0
- power_loop/tools/default_manifest.py +244 -0
- power_loop/tools/default_tools.py +766 -0
- power_loop/tools/registry.py +162 -0
- power_loop/tools/spawn_agent.py +173 -0
- power_loop-0.2.0.dist-info/METADATA +632 -0
- power_loop-0.2.0.dist-info/RECORD +53 -0
- power_loop-0.2.0.dist-info/WHEEL +5 -0
- power_loop-0.2.0.dist-info/licenses/LICENSE +21 -0
- power_loop-0.2.0.dist-info/top_level.txt +2 -0
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
"""Composable system prompt builder for power-loop agents.
|
|
2
|
+
|
|
3
|
+
Design goals
|
|
4
|
+
------------
|
|
5
|
+
* **Library-friendly**: nothing is hard-coded. Users can fully replace the
|
|
6
|
+
prompt, cherry-pick built-in sections, or append custom sections.
|
|
7
|
+
* **Runtime-aware**: ``SystemPromptContext`` carries workspace paths, tool
|
|
8
|
+
list, skill descriptions, model name, etc. Sections that need this info
|
|
9
|
+
receive it automatically.
|
|
10
|
+
* **Backward-compatible**: the old ``build_agent_system_prompt(ctx)`` API
|
|
11
|
+
still works.
|
|
12
|
+
"""
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from collections import OrderedDict
|
|
16
|
+
from collections.abc import Callable, Sequence
|
|
17
|
+
from dataclasses import dataclass
|
|
18
|
+
|
|
19
|
+
# ---------------------------------------------------------------------------
|
|
20
|
+
# Context that sections can reference
|
|
21
|
+
# ---------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class SystemPromptContext:
|
|
25
|
+
"""Runtime context injected into prompt sections.
|
|
26
|
+
|
|
27
|
+
All fields are optional so callers only supply what they have.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
model: str = ""
|
|
31
|
+
workspace_dir: str = ""
|
|
32
|
+
agent_dir: str = ""
|
|
33
|
+
skills_dir: str = ""
|
|
34
|
+
skill_descriptions: str = ""
|
|
35
|
+
tool_names: Sequence[str] = ()
|
|
36
|
+
extra: str = ""
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# ---------------------------------------------------------------------------
|
|
40
|
+
# Built-in prompt sections (each is a plain function: ctx -> str | None)
|
|
41
|
+
# ---------------------------------------------------------------------------
|
|
42
|
+
SectionRenderer = Callable[[SystemPromptContext], str | None]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def section_identity(ctx: SystemPromptContext) -> str:
|
|
46
|
+
parts = ["You are an interactive coding agent."]
|
|
47
|
+
if ctx.model:
|
|
48
|
+
parts.append(f"Model: {ctx.model}")
|
|
49
|
+
return "\n".join(parts)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def section_style(_ctx: SystemPromptContext) -> str:
|
|
53
|
+
return (
|
|
54
|
+
"# Tone and Style\n"
|
|
55
|
+
"- Be concise and direct; use markdown sparingly.\n"
|
|
56
|
+
"- Prioritize technical accuracy. Respectful correction is more valuable than false agreement.\n"
|
|
57
|
+
"- Never propose changes to code you haven't read. Always read first, then modify.\n"
|
|
58
|
+
"- Avoid over-engineering. Only make changes directly requested or clearly necessary.\n"
|
|
59
|
+
" - Don't add features, refactoring, comments, or type annotations beyond what was asked.\n"
|
|
60
|
+
" - Don't add error handling for scenarios that can't happen.\n"
|
|
61
|
+
" - Don't create abstractions for one-time operations."
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def section_workflow(_ctx: SystemPromptContext) -> str:
|
|
66
|
+
return (
|
|
67
|
+
"# Workflow\n"
|
|
68
|
+
"1. Understand: read relevant files before acting. Use glob/grep to find files, not bash.\n"
|
|
69
|
+
"2. Plan: for multi-step tasks (3+ steps), create a todo list FIRST.\n"
|
|
70
|
+
"3. Execute: work through items one at a time. Mark each in_progress before starting, completed when done.\n"
|
|
71
|
+
"4. Verify: after edits, run tests or linters when appropriate."
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def section_security(_ctx: SystemPromptContext) -> str:
|
|
76
|
+
return (
|
|
77
|
+
"# Security\n"
|
|
78
|
+
"- Never request passwords, tokens, API keys, or any secrets via chat.\n"
|
|
79
|
+
"- Do NOT simulate interactive password prompts. If a command needs sudo, explain what the user should run manually."
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def section_tool_guide(ctx: SystemPromptContext) -> str | None:
|
|
84
|
+
"""Per-tool usage hints. Only emits tools that are actually registered."""
|
|
85
|
+
catalog: dict[str, str] = {
|
|
86
|
+
"bash": (
|
|
87
|
+
"persistent session — cwd and env vars survive across calls. "
|
|
88
|
+
"Use restart=true to reset. Avoid dangerous commands (rm -rf /, sudo)."
|
|
89
|
+
),
|
|
90
|
+
"read_file": (
|
|
91
|
+
'returns numbered lines (" 1|code"). Use offset/limit for large files. '
|
|
92
|
+
"Pass a directory path to list contents. Always read before editing."
|
|
93
|
+
),
|
|
94
|
+
"write_file": (
|
|
95
|
+
"create or overwrite a file. Both 'path' and 'content' are REQUIRED in one JSON object. "
|
|
96
|
+
"Prefer edit_file or apply_patch for existing files."
|
|
97
|
+
),
|
|
98
|
+
"edit_file": (
|
|
99
|
+
"str_replace (old_text -> new_text). old_text must be unique — include more context if ambiguous. "
|
|
100
|
+
"Set replace_all=true to replace every occurrence. You MUST read_file before editing."
|
|
101
|
+
),
|
|
102
|
+
"apply_patch": (
|
|
103
|
+
"apply a patch using @@ context lines for positioning and +/- for line changes. "
|
|
104
|
+
"You MUST read_file before patching. Best for large edits."
|
|
105
|
+
),
|
|
106
|
+
"glob": "find files by pattern (e.g. '*.py', '**/*.ts'). Prefer over bash find.",
|
|
107
|
+
"grep": "search file contents by regex. Prefer over bash grep/rg. Supports include filter.",
|
|
108
|
+
"load_skill": "load specialized knowledge before tackling unfamiliar domains.",
|
|
109
|
+
"todo": (
|
|
110
|
+
"track multi-step tasks. Keep exactly one item in_progress at a time. "
|
|
111
|
+
"Mark in_progress BEFORE starting, completed IMMEDIATELY when done."
|
|
112
|
+
),
|
|
113
|
+
"background_run": "run a shell command asynchronously in a background worker.",
|
|
114
|
+
"check_background": "inspect status/output of background tasks by task_id or list all.",
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
names = ctx.tool_names if ctx.tool_names else list(catalog.keys())
|
|
118
|
+
lines = ["# Tool Usage"]
|
|
119
|
+
lines.append(
|
|
120
|
+
"CRITICAL — File path fidelity: When passing file paths to ANY tool, "
|
|
121
|
+
"use the EXACT path string as given. NEVER rename, re-space, or re-punctuate file names."
|
|
122
|
+
)
|
|
123
|
+
for name in names:
|
|
124
|
+
hint = catalog.get(name)
|
|
125
|
+
if hint:
|
|
126
|
+
lines.append(f"- {name}: {hint}")
|
|
127
|
+
return "\n".join(lines)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def section_paths(ctx: SystemPromptContext) -> str | None:
|
|
131
|
+
if not ctx.workspace_dir:
|
|
132
|
+
return None
|
|
133
|
+
lines = [
|
|
134
|
+
"# Paths — CRITICAL",
|
|
135
|
+
f"- **Workspace (working directory)**: {ctx.workspace_dir}",
|
|
136
|
+
" All file operations default to this directory. Relative paths resolve here.",
|
|
137
|
+
]
|
|
138
|
+
if ctx.agent_dir:
|
|
139
|
+
lines.append(
|
|
140
|
+
f"- Agent home (library installation): {ctx.agent_dir}\n"
|
|
141
|
+
" Agent-home internals are restricted. "
|
|
142
|
+
"Allowlisted paths: .cache, logs, and skills directory."
|
|
143
|
+
)
|
|
144
|
+
lines.append(
|
|
145
|
+
f"- IMPORTANT: Do NOT confuse workspace ({ctx.workspace_dir}) with agent home ({ctx.agent_dir})."
|
|
146
|
+
)
|
|
147
|
+
if ctx.skills_dir:
|
|
148
|
+
lines.append(f"- Skills directory: {ctx.skills_dir}")
|
|
149
|
+
lines.append(
|
|
150
|
+
"- Use @workspace/<path> or @agent/<path> when an explicit root is needed, "
|
|
151
|
+
"but prefer plain relative paths (they default to workspace)."
|
|
152
|
+
)
|
|
153
|
+
return "\n".join(lines)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def section_todo_discipline(_ctx: SystemPromptContext) -> str:
|
|
157
|
+
return (
|
|
158
|
+
"# Todo Discipline\n"
|
|
159
|
+
"- Use the todo tool for any task with 3+ steps.\n"
|
|
160
|
+
"- Mark a todo in_progress BEFORE you start working on it.\n"
|
|
161
|
+
"- Mark it completed IMMEDIATELY when done — do not batch.\n"
|
|
162
|
+
"- Only one item should be in_progress at any time."
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def section_skills(ctx: SystemPromptContext) -> str | None:
|
|
167
|
+
if not ctx.skill_descriptions or ctx.skill_descriptions == "(no skills available)":
|
|
168
|
+
return None
|
|
169
|
+
lines = [
|
|
170
|
+
"# Skills",
|
|
171
|
+
f"Skills directory: {ctx.skills_dir or '(default)'}",
|
|
172
|
+
"Use load_skill(name) to load a skill by name.",
|
|
173
|
+
ctx.skill_descriptions,
|
|
174
|
+
]
|
|
175
|
+
return "\n".join(lines)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
# Ordered registry of all built-in sections.
|
|
179
|
+
BUILTIN_SECTIONS: dict[str, SectionRenderer] = OrderedDict([
|
|
180
|
+
("identity", section_identity),
|
|
181
|
+
("style", section_style),
|
|
182
|
+
("workflow", section_workflow),
|
|
183
|
+
("security", section_security),
|
|
184
|
+
("tool_guide", section_tool_guide),
|
|
185
|
+
("paths", section_paths),
|
|
186
|
+
("todo_discipline", section_todo_discipline),
|
|
187
|
+
("skills", section_skills),
|
|
188
|
+
])
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
# ---------------------------------------------------------------------------
|
|
192
|
+
# Builder
|
|
193
|
+
# ---------------------------------------------------------------------------
|
|
194
|
+
|
|
195
|
+
class SystemPromptBuilder:
|
|
196
|
+
"""Composable system prompt builder.
|
|
197
|
+
|
|
198
|
+
Examples::
|
|
199
|
+
|
|
200
|
+
# Use all built-in sections with runtime context
|
|
201
|
+
ctx = SystemPromptContext(model="gpt-4o", workspace_dir="/app")
|
|
202
|
+
prompt = SystemPromptBuilder().build(ctx)
|
|
203
|
+
|
|
204
|
+
# Cherry-pick sections
|
|
205
|
+
prompt = (
|
|
206
|
+
SystemPromptBuilder()
|
|
207
|
+
.use("identity", "tool_guide", "paths")
|
|
208
|
+
.build(ctx)
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
# Add custom section
|
|
212
|
+
prompt = (
|
|
213
|
+
SystemPromptBuilder()
|
|
214
|
+
.add("my_rules", lambda ctx: "Always respond in Chinese.")
|
|
215
|
+
.build(ctx)
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
# Fully custom — ignore built-ins
|
|
219
|
+
prompt = (
|
|
220
|
+
SystemPromptBuilder(use_defaults=False)
|
|
221
|
+
.add("custom", lambda ctx: f"You work in {ctx.workspace_dir}")
|
|
222
|
+
.build(ctx)
|
|
223
|
+
)
|
|
224
|
+
"""
|
|
225
|
+
|
|
226
|
+
def __init__(self, *, use_defaults: bool = True) -> None:
|
|
227
|
+
self._sections: OrderedDict[str, SectionRenderer] = OrderedDict()
|
|
228
|
+
if use_defaults:
|
|
229
|
+
self._sections.update(BUILTIN_SECTIONS)
|
|
230
|
+
|
|
231
|
+
def use(self, *names: str) -> SystemPromptBuilder:
|
|
232
|
+
"""Keep only the named built-in sections (in given order)."""
|
|
233
|
+
selected = OrderedDict()
|
|
234
|
+
for name in names:
|
|
235
|
+
renderer = BUILTIN_SECTIONS.get(name) or self._sections.get(name)
|
|
236
|
+
if renderer is None:
|
|
237
|
+
raise ValueError(
|
|
238
|
+
f"Unknown section '{name}'. "
|
|
239
|
+
f"Built-in: {', '.join(BUILTIN_SECTIONS)}"
|
|
240
|
+
)
|
|
241
|
+
selected[name] = renderer
|
|
242
|
+
self._sections = selected
|
|
243
|
+
return self
|
|
244
|
+
|
|
245
|
+
def add(
|
|
246
|
+
self,
|
|
247
|
+
name: str,
|
|
248
|
+
renderer: SectionRenderer | str,
|
|
249
|
+
*,
|
|
250
|
+
before: str | None = None,
|
|
251
|
+
after: str | None = None,
|
|
252
|
+
) -> SystemPromptBuilder:
|
|
253
|
+
"""Add or replace a section.
|
|
254
|
+
|
|
255
|
+
*renderer* can be a callable ``(ctx) -> str`` or a plain string.
|
|
256
|
+
Use *before*/*after* to control position relative to an existing section.
|
|
257
|
+
"""
|
|
258
|
+
fn: SectionRenderer
|
|
259
|
+
if isinstance(renderer, str):
|
|
260
|
+
_renderer_str: str = renderer
|
|
261
|
+
|
|
262
|
+
def fn(_c: SystemPromptContext, _s: str = _renderer_str) -> str:
|
|
263
|
+
return _s
|
|
264
|
+
else:
|
|
265
|
+
fn = renderer
|
|
266
|
+
|
|
267
|
+
if before or after:
|
|
268
|
+
anchor = before or after
|
|
269
|
+
new = OrderedDict()
|
|
270
|
+
inserted = False
|
|
271
|
+
for k, v in self._sections.items():
|
|
272
|
+
if k == anchor and before:
|
|
273
|
+
new[name] = fn
|
|
274
|
+
inserted = True
|
|
275
|
+
new[k] = v
|
|
276
|
+
if k == anchor and after:
|
|
277
|
+
new[name] = fn
|
|
278
|
+
inserted = True
|
|
279
|
+
if not inserted:
|
|
280
|
+
new[name] = fn
|
|
281
|
+
self._sections = new
|
|
282
|
+
else:
|
|
283
|
+
self._sections[name] = fn
|
|
284
|
+
return self
|
|
285
|
+
|
|
286
|
+
def remove(self, *names: str) -> SystemPromptBuilder:
|
|
287
|
+
"""Remove sections by name."""
|
|
288
|
+
for name in names:
|
|
289
|
+
self._sections.pop(name, None)
|
|
290
|
+
return self
|
|
291
|
+
|
|
292
|
+
def build(self, ctx: SystemPromptContext) -> str:
|
|
293
|
+
"""Render all sections into a single prompt string."""
|
|
294
|
+
parts: list[str] = []
|
|
295
|
+
for _name, renderer in self._sections.items():
|
|
296
|
+
result = renderer(ctx)
|
|
297
|
+
if result and result.strip():
|
|
298
|
+
parts.append(result.strip())
|
|
299
|
+
return "\n\n".join(parts) + "\n"
|
|
300
|
+
|
|
301
|
+
def section_names(self) -> list[str]:
|
|
302
|
+
"""Return current section names in order."""
|
|
303
|
+
return list(self._sections.keys())
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
# ---------------------------------------------------------------------------
|
|
307
|
+
# Convenience defaults (backward-compatible)
|
|
308
|
+
# ---------------------------------------------------------------------------
|
|
309
|
+
|
|
310
|
+
DEFAULT_AGENT_SYSTEM_PROMPT = SystemPromptBuilder().build(SystemPromptContext())
|
|
311
|
+
|
|
312
|
+
DEFAULT_SUBAGENT_SYSTEM_PROMPT = (
|
|
313
|
+
SystemPromptBuilder(use_defaults=False)
|
|
314
|
+
.add("identity", lambda _: (
|
|
315
|
+
"You are a coding subagent.\n\n"
|
|
316
|
+
"Execution mode:\n"
|
|
317
|
+
"- Complete the assigned task autonomously.\n"
|
|
318
|
+
"- Keep outputs concise and evidence-based.\n"
|
|
319
|
+
"- Return key findings with exact file paths and line anchors when possible."
|
|
320
|
+
))
|
|
321
|
+
.add("paths", section_paths)
|
|
322
|
+
).build(SystemPromptContext())
|
|
323
|
+
|
|
324
|
+
DEFAULT_EXPLORE_SUBAGENT_SYSTEM_PROMPT = (
|
|
325
|
+
SystemPromptBuilder(use_defaults=False)
|
|
326
|
+
.add("identity", lambda _: (
|
|
327
|
+
"You are a read-only exploration subagent.\n\n"
|
|
328
|
+
"Execution mode:\n"
|
|
329
|
+
"- Search and read files only; do not modify code.\n"
|
|
330
|
+
"- Summarize findings with concrete references.\n"
|
|
331
|
+
"- Flag uncertainties explicitly."
|
|
332
|
+
))
|
|
333
|
+
.add("paths", section_paths)
|
|
334
|
+
).build(SystemPromptContext())
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def build_agent_system_prompt(
|
|
338
|
+
ctx: SystemPromptContext,
|
|
339
|
+
extra: str | None = None,
|
|
340
|
+
*,
|
|
341
|
+
builder: SystemPromptBuilder | None = None,
|
|
342
|
+
) -> str:
|
|
343
|
+
"""Build a full agent system prompt with runtime context.
|
|
344
|
+
|
|
345
|
+
If *builder* is ``None`` a default builder with all sections is used.
|
|
346
|
+
*extra* is appended at the end.
|
|
347
|
+
"""
|
|
348
|
+
b = builder if builder is not None else SystemPromptBuilder()
|
|
349
|
+
if extra and extra.strip():
|
|
350
|
+
b = SystemPromptBuilder(use_defaults=False)
|
|
351
|
+
# Copy sections from source builder
|
|
352
|
+
for name in (builder or SystemPromptBuilder()).section_names():
|
|
353
|
+
section = BUILTIN_SECTIONS.get(name)
|
|
354
|
+
if section:
|
|
355
|
+
b.add(name, section)
|
|
356
|
+
b.add("extra", extra.strip())
|
|
357
|
+
return b.build(ctx)
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def build_subagent_system_prompt(
|
|
361
|
+
ctx: SystemPromptContext,
|
|
362
|
+
extra: str | None = None,
|
|
363
|
+
) -> str:
|
|
364
|
+
b = (
|
|
365
|
+
SystemPromptBuilder(use_defaults=False)
|
|
366
|
+
.add("identity", lambda _: (
|
|
367
|
+
"You are a coding subagent.\n\n"
|
|
368
|
+
"- Complete the assigned task autonomously.\n"
|
|
369
|
+
"- Keep outputs concise and evidence-based.\n"
|
|
370
|
+
"- Return key findings with exact file paths and line anchors."
|
|
371
|
+
))
|
|
372
|
+
.add("paths", section_paths)
|
|
373
|
+
)
|
|
374
|
+
if extra and extra.strip():
|
|
375
|
+
b.add("extra", extra.strip())
|
|
376
|
+
return b.build(ctx)
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def build_explore_subagent_system_prompt(
|
|
380
|
+
ctx: SystemPromptContext,
|
|
381
|
+
extra: str | None = None,
|
|
382
|
+
) -> str:
|
|
383
|
+
b = (
|
|
384
|
+
SystemPromptBuilder(use_defaults=False)
|
|
385
|
+
.add("identity", lambda _: (
|
|
386
|
+
"You are a read-only exploration subagent.\n\n"
|
|
387
|
+
"- Search and read files only; do not modify code.\n"
|
|
388
|
+
"- Summarize findings with concrete references.\n"
|
|
389
|
+
"- Flag uncertainties explicitly."
|
|
390
|
+
))
|
|
391
|
+
.add("tool_guide", section_tool_guide)
|
|
392
|
+
.add("paths", section_paths)
|
|
393
|
+
)
|
|
394
|
+
if extra and extra.strip():
|
|
395
|
+
b.add("extra", extra.strip())
|
|
396
|
+
return b.build(ctx)
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from typing import TYPE_CHECKING, Any, Literal
|
|
5
|
+
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
from power_loop.runtime.compact import Compactor
|
|
8
|
+
from power_loop.runtime.memory import MemoryProvider
|
|
9
|
+
from power_loop.runtime.retry import LLMRetryPolicy
|
|
10
|
+
|
|
11
|
+
LoopStatus = Literal["completed", "pending_tools", "cancelled", "hit_round_limit", "degraded"]
|
|
12
|
+
LoopMessage = dict[str, Any]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _default_compactor() -> Compactor:
|
|
16
|
+
from power_loop.runtime.compact import DefaultCompactor
|
|
17
|
+
return DefaultCompactor()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class AgentLoopConfig:
|
|
22
|
+
"""Configuration for the agent loop."""
|
|
23
|
+
|
|
24
|
+
system_prompt: str | None = None
|
|
25
|
+
max_rounds: int = 24
|
|
26
|
+
temperature: float | None = 0.0
|
|
27
|
+
max_tokens: int | None = 8000
|
|
28
|
+
compactor: Compactor | None = field(default_factory=_default_compactor)
|
|
29
|
+
retry_policy: LLMRetryPolicy | None = None
|
|
30
|
+
memory: MemoryProvider | None = None
|
|
31
|
+
memory_budget_tokens: int = 1500
|
|
32
|
+
skills_dir: str | None = None
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass
|
|
36
|
+
class AgentLoopResult:
|
|
37
|
+
status: LoopStatus
|
|
38
|
+
final_text: str = ""
|
|
39
|
+
rounds: int = 0
|
|
40
|
+
pending_tool_calls: list[dict[str, Any]] = field(default_factory=list)
|
|
41
|
+
messages: list[LoopMessage] = field(default_factory=list)
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"""Core contracts for agent runtime (types/protocols/constants only)."""
|
|
2
|
+
|
|
3
|
+
from power_loop.contracts.event_payloads import (
|
|
4
|
+
AgentErrorPayload,
|
|
5
|
+
AutoCompactStatusPayload,
|
|
6
|
+
BaseEventPayload,
|
|
7
|
+
HitRoundLimitStatusPayload,
|
|
8
|
+
RoundCompletedPayload,
|
|
9
|
+
RoundStartedPayload,
|
|
10
|
+
RoundToolsPresentPayload,
|
|
11
|
+
RoundUsageStatusPayload,
|
|
12
|
+
SessionEndedPayload,
|
|
13
|
+
SessionStartedPayload,
|
|
14
|
+
StatusChangedPayload,
|
|
15
|
+
StreamCompletedPayload,
|
|
16
|
+
StreamDeltaPayload,
|
|
17
|
+
StreamStartedPayload,
|
|
18
|
+
SubagentCompletedPayload,
|
|
19
|
+
SubagentLimitPayload,
|
|
20
|
+
SubagentTaskStartPayload,
|
|
21
|
+
SubagentTextPayload,
|
|
22
|
+
SystemLogPayload,
|
|
23
|
+
TodoUpdatedPayload,
|
|
24
|
+
ToolCallCompletedPayload,
|
|
25
|
+
ToolCallFailedPayload,
|
|
26
|
+
ToolCallStartedPayload,
|
|
27
|
+
UsageUpdatedPayload,
|
|
28
|
+
UserNotificationPayload,
|
|
29
|
+
)
|
|
30
|
+
from power_loop.contracts.events import AgentEvent, AgentEventType
|
|
31
|
+
from power_loop.contracts.handlers import (
|
|
32
|
+
EventHandler,
|
|
33
|
+
HookHandler,
|
|
34
|
+
ToolHandler,
|
|
35
|
+
ToolHandlerResult,
|
|
36
|
+
)
|
|
37
|
+
from power_loop.contracts.hook_contexts import (
|
|
38
|
+
BaseHookCtx,
|
|
39
|
+
CompactAfterCtx,
|
|
40
|
+
CompactBeforeCtx,
|
|
41
|
+
LlmAfterCtx,
|
|
42
|
+
LlmBeforeCtx,
|
|
43
|
+
MessageAppendCtx,
|
|
44
|
+
RoundDecideCtx,
|
|
45
|
+
RoundEndCtx,
|
|
46
|
+
RoundStartCtx,
|
|
47
|
+
SessionEndCtx,
|
|
48
|
+
SessionStartCtx,
|
|
49
|
+
ToolAfterCtx,
|
|
50
|
+
ToolBeforeCtx,
|
|
51
|
+
ToolErrorCtx,
|
|
52
|
+
ToolsBatchAfterCtx,
|
|
53
|
+
ToolsBatchBeforeCtx,
|
|
54
|
+
)
|
|
55
|
+
from power_loop.contracts.hooks import HookContext, HookDirective, HookPoint, HookResult
|
|
56
|
+
from power_loop.contracts.messages import (
|
|
57
|
+
AgentMessage,
|
|
58
|
+
AssistantMessage,
|
|
59
|
+
MessageRole,
|
|
60
|
+
SystemMessage,
|
|
61
|
+
ToolCall,
|
|
62
|
+
ToolResultMessage,
|
|
63
|
+
UserMessage,
|
|
64
|
+
)
|
|
65
|
+
from power_loop.contracts.protocols import EventBusProtocol, HookManagerProtocol, ToolArgsValidator
|
|
66
|
+
from power_loop.contracts.tools import ToolDefinition, validate_tool_args
|
|
67
|
+
|
|
68
|
+
__all__ = [
|
|
69
|
+
"AgentMessage",
|
|
70
|
+
"MessageRole",
|
|
71
|
+
"ToolCall",
|
|
72
|
+
"ToolResultMessage",
|
|
73
|
+
"UserMessage",
|
|
74
|
+
"AssistantMessage",
|
|
75
|
+
"SystemMessage",
|
|
76
|
+
"AgentEvent",
|
|
77
|
+
"AgentEventType",
|
|
78
|
+
"HookContext",
|
|
79
|
+
"HookDirective",
|
|
80
|
+
"HookPoint",
|
|
81
|
+
"HookResult",
|
|
82
|
+
"BaseHookCtx",
|
|
83
|
+
"CompactAfterCtx",
|
|
84
|
+
"CompactBeforeCtx",
|
|
85
|
+
"LlmAfterCtx",
|
|
86
|
+
"LlmBeforeCtx",
|
|
87
|
+
"MessageAppendCtx",
|
|
88
|
+
"RoundDecideCtx",
|
|
89
|
+
"RoundEndCtx",
|
|
90
|
+
"RoundStartCtx",
|
|
91
|
+
"SessionEndCtx",
|
|
92
|
+
"SessionStartCtx",
|
|
93
|
+
"ToolAfterCtx",
|
|
94
|
+
"ToolBeforeCtx",
|
|
95
|
+
"ToolErrorCtx",
|
|
96
|
+
"ToolsBatchAfterCtx",
|
|
97
|
+
"ToolsBatchBeforeCtx",
|
|
98
|
+
"BaseEventPayload",
|
|
99
|
+
"SessionStartedPayload",
|
|
100
|
+
"SessionEndedPayload",
|
|
101
|
+
"RoundStartedPayload",
|
|
102
|
+
"RoundCompletedPayload",
|
|
103
|
+
"RoundToolsPresentPayload",
|
|
104
|
+
"StreamStartedPayload",
|
|
105
|
+
"StreamDeltaPayload",
|
|
106
|
+
"StreamCompletedPayload",
|
|
107
|
+
"ToolCallStartedPayload",
|
|
108
|
+
"ToolCallCompletedPayload",
|
|
109
|
+
"ToolCallFailedPayload",
|
|
110
|
+
"StatusChangedPayload",
|
|
111
|
+
"AutoCompactStatusPayload",
|
|
112
|
+
"RoundUsageStatusPayload",
|
|
113
|
+
"HitRoundLimitStatusPayload",
|
|
114
|
+
"UsageUpdatedPayload",
|
|
115
|
+
"TodoUpdatedPayload",
|
|
116
|
+
"UserNotificationPayload",
|
|
117
|
+
"AgentErrorPayload",
|
|
118
|
+
"SystemLogPayload",
|
|
119
|
+
"SubagentTaskStartPayload",
|
|
120
|
+
"SubagentTextPayload",
|
|
121
|
+
"SubagentLimitPayload",
|
|
122
|
+
"SubagentCompletedPayload",
|
|
123
|
+
"EventHandler",
|
|
124
|
+
"HookHandler",
|
|
125
|
+
"ToolHandler",
|
|
126
|
+
"ToolHandlerResult",
|
|
127
|
+
"EventBusProtocol",
|
|
128
|
+
"HookManagerProtocol",
|
|
129
|
+
"ToolArgsValidator",
|
|
130
|
+
"ToolDefinition",
|
|
131
|
+
"validate_tool_args",
|
|
132
|
+
]
|