@ssheleg/agent-stack 0.8.0 → 0.10.0
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.
- package/CHANGELOG.md +132 -0
- package/README.md +18 -4
- package/package.json +1 -1
- package/plugins/agent-stack/.claude-plugin/plugin.json +1 -1
- package/plugins/agent-stack/skills/agent-evals/SKILL.md +44 -3
- package/plugins/agent-stack/skills/agent-harness/SKILL.md +37 -9
- package/plugins/agent-stack/skills/agent-harness/references/layers.md +4 -0
- package/plugins/agent-stack/skills/agent-harness/references/pi-sdk.md +318 -0
- package/plugins/agent-stack/skills/agent-harness/references/pi.md +241 -0
- package/plugins/agent-stack/skills/agent-harness/scripts/audit_agent.py +100 -32
- package/plugins/agent-stack/skills/agent-orchestrator/SKILL.md +100 -101
- package/plugins/agent-stack/skills/agent-orchestrator/references/graph-engineering.md +327 -0
- package/plugins/agent-stack/skills/agent-orchestrator/references/llm-proxy-billing.md +1 -1
- package/plugins/agent-stack/skills/agent-orchestrator/references/patterns.md +38 -14
|
@@ -158,14 +158,50 @@ def check_hardcoded_model(rel, text, lines):
|
|
|
158
158
|
"tenant, system default — is the shape that bills correctly")
|
|
159
159
|
|
|
160
160
|
|
|
161
|
+
def check_unguarded_fanout(rel, text, lines):
|
|
162
|
+
"""A fan-out whose siblings' failures are not captured.
|
|
163
|
+
|
|
164
|
+
`asyncio.gather` without `return_exceptions=True` cancels the whole batch on the first
|
|
165
|
+
exception: every other branch's completed work is discarded, and the node that consumes
|
|
166
|
+
the results cannot tell a branch that FAILED from one that returned nothing. `Promise.all`
|
|
167
|
+
has the identical shape. That is the failure a checker node between a parallel layer and
|
|
168
|
+
its convergence exists to stop, and it is invisible in a green test run because the happy
|
|
169
|
+
path never exercises it.
|
|
170
|
+
|
|
171
|
+
Conservative twice over, like every detector here: the file must already look
|
|
172
|
+
agent-related, and the capturing form must be absent from the WHOLE file — one
|
|
173
|
+
`return_exceptions` or `allSettled` anywhere is taken as evidence the author knows the
|
|
174
|
+
distinction, and this pass says nothing.
|
|
175
|
+
"""
|
|
176
|
+
if re.search(r"return_exceptions|allSettled", text):
|
|
177
|
+
return
|
|
178
|
+
for i, l in enumerate(lines, 1):
|
|
179
|
+
if re.search(r"\basyncio\.gather\s*\(", l):
|
|
180
|
+
add("unguarded-fanout", rel, i,
|
|
181
|
+
"`asyncio.gather` with no `return_exceptions=True` — the first sibling to "
|
|
182
|
+
"raise cancels the batch and throws away what the others already produced",
|
|
183
|
+
"Capture each branch's outcome, then gate the convergence on a checker that "
|
|
184
|
+
"can tell a failed branch from an empty one")
|
|
185
|
+
if re.search(r"\bPromise\.all\s*\(", l) and ".catch" not in l:
|
|
186
|
+
add("unguarded-fanout", rel, i,
|
|
187
|
+
"`Promise.all` with no `allSettled` and no per-branch `.catch` — one "
|
|
188
|
+
"rejection discards every other branch's completed result",
|
|
189
|
+
"Use `Promise.allSettled` or catch per branch, then gate the convergence on "
|
|
190
|
+
"a checker that can see which branch failed")
|
|
191
|
+
|
|
192
|
+
|
|
161
193
|
CHECKS = [check_unbounded_loop, check_tool_without_description, check_swallowed_error,
|
|
162
|
-
check_no_timeout, check_hardcoded_model]
|
|
194
|
+
check_no_timeout, check_hardcoded_model, check_unguarded_fanout]
|
|
163
195
|
|
|
164
196
|
# What no static pass can reach. Printed every run, never suppressed.
|
|
165
197
|
BLIND = [
|
|
166
198
|
"whether the SYSTEM PROMPT is at the right altitude — or whether it is in this repo at all",
|
|
167
199
|
"whether two tool descriptions actually distinguish themselves to a model",
|
|
168
200
|
"whether the workflow/agent choice was made deliberately or defaulted to an agent",
|
|
201
|
+
"whether a fan-out has a CHECKER between it and the node that consumes it — this pass "
|
|
202
|
+
"sees an unguarded gather, never a missing gate",
|
|
203
|
+
"whether a declared dependency graph is actually executed in dependency order, or in "
|
|
204
|
+
"the order the stages happen to be listed in",
|
|
169
205
|
"whether retries and fallbacks MULTIPLY (three providers x three retries is nine calls)",
|
|
170
206
|
"whether compaction preserves decisions and open questions, or keeps the discussion",
|
|
171
207
|
"whether tool output is treated as untrusted input",
|
|
@@ -217,54 +253,86 @@ def report_text(root, seen, considered):
|
|
|
217
253
|
return "\n".join(out)
|
|
218
254
|
|
|
219
255
|
|
|
256
|
+
PY_HEADER = ("import requests\n"
|
|
257
|
+
"system_prompt = 'x'\n"
|
|
258
|
+
"tools = [{'name': 't', 'description': 'does a thing'}]\n"
|
|
259
|
+
"messages = []\n")
|
|
260
|
+
JS_HEADER = ("const system_prompt = 'x';\n"
|
|
261
|
+
"const tools = [{name: 't', description: 'does a thing'}];\n"
|
|
262
|
+
"const messages = [];\n")
|
|
263
|
+
|
|
264
|
+
# (label, the check that MUST fire, filename, body)
|
|
265
|
+
PLANTS = [
|
|
266
|
+
("unbounded-loop", "unbounded-loop", "agent.py",
|
|
267
|
+
PY_HEADER + "while True:\n pass\n"),
|
|
268
|
+
("tool-no-description", "tool-no-description", "agent.py",
|
|
269
|
+
PY_HEADER + "T = [{'name': 'a', 'description': ''}]\n"),
|
|
270
|
+
("swallowed-error", "swallowed-error", "agent.py",
|
|
271
|
+
PY_HEADER + "try:\n x = 1\nexcept Exception:\n pass\n"),
|
|
272
|
+
("no-timeout", "no-timeout", "agent.py",
|
|
273
|
+
PY_HEADER + "r = requests.get('https://example.com')\n"),
|
|
274
|
+
("hardcoded-model", "hardcoded-model", "agent.py",
|
|
275
|
+
PY_HEADER + "a = 'claude-opus-4'\nb = 'claude-opus-4'\n"),
|
|
276
|
+
("unguarded-fanout (asyncio)", "unguarded-fanout", "agent.py",
|
|
277
|
+
PY_HEADER + "out = await asyncio.gather(*(run(t) for t in tasks))\n"),
|
|
278
|
+
("unguarded-fanout (promise)", "unguarded-fanout", "agent.js",
|
|
279
|
+
JS_HEADER + "const out = await Promise.all(tasks.map(t => run(t)));\n"),
|
|
280
|
+
]
|
|
281
|
+
|
|
282
|
+
# A detector that fires on the defect AND on its fix has no discriminating power. Each
|
|
283
|
+
# clean fixture is the half of the evidence that says which one this is.
|
|
284
|
+
CLEAN = [
|
|
285
|
+
("the ordinary correct file", "agent.py",
|
|
286
|
+
PY_HEADER + "for _ in range(10):\n pass\n"
|
|
287
|
+
"r = requests.get('https://example.com', timeout=5)\n"),
|
|
288
|
+
("a fan-out that DOES capture its branches", "agent.py",
|
|
289
|
+
PY_HEADER + "for _ in range(10):\n pass\n"
|
|
290
|
+
"out = await asyncio.gather(*(run(t) for t in tasks), return_exceptions=True)\n"),
|
|
291
|
+
]
|
|
292
|
+
|
|
293
|
+
|
|
220
294
|
def self_test():
|
|
221
295
|
"""Plant each defect and require the matching check to fire.
|
|
222
296
|
|
|
223
297
|
A detector nobody has watched fire is not evidence that it works, and every plant
|
|
224
298
|
asserts it changed something so a reworded fixture fails HERE rather than reporting a
|
|
225
299
|
healthy checker as broken.
|
|
300
|
+
|
|
301
|
+
Two things the shape of this function is deliberate about. A detector that reads two
|
|
302
|
+
languages gets a plant in **each** — one passing shape is not evidence about the
|
|
303
|
+
other. And every run also asserts silence on the CORRECT shape of the same defect,
|
|
304
|
+
which is what separates a detector from a keyword search.
|
|
226
305
|
"""
|
|
227
306
|
import tempfile
|
|
228
|
-
header = ("import requests\n"
|
|
229
|
-
"system_prompt = 'x'\n"
|
|
230
|
-
"tools = [{'name': 't', 'description': 'does a thing'}]\n"
|
|
231
|
-
"messages = []\n")
|
|
232
|
-
cases = {
|
|
233
|
-
"unbounded-loop": header + "while True:\n pass\n",
|
|
234
|
-
"tool-no-description": header + "T = [{'name': 'a', 'description': ''}]\n",
|
|
235
|
-
"swallowed-error": header + "try:\n x = 1\nexcept Exception:\n pass\n",
|
|
236
|
-
"no-timeout": header + "r = requests.get('https://example.com')\n",
|
|
237
|
-
"hardcoded-model": header + "a = 'claude-opus-4'\nb = 'claude-opus-4'\n",
|
|
238
|
-
}
|
|
239
307
|
failures = 0
|
|
240
|
-
for kind, body in
|
|
308
|
+
for label, kind, fname, body in PLANTS:
|
|
241
309
|
FINDINGS.clear()
|
|
242
310
|
with tempfile.TemporaryDirectory() as d:
|
|
243
|
-
|
|
244
|
-
with open(p, "w", encoding="utf-8") as fh:
|
|
311
|
+
with open(os.path.join(d, fname), "w", encoding="utf-8") as fh:
|
|
245
312
|
fh.write(body)
|
|
246
|
-
assert agentish(body), f"PLANT DID NOT LAND: fixture for {
|
|
313
|
+
assert agentish(body), f"PLANT DID NOT LAND: fixture for {label} is not agent-related"
|
|
247
314
|
scan(d)
|
|
248
315
|
got = {f["check"] for f in FINDINGS}
|
|
249
316
|
if kind in got:
|
|
250
|
-
print(f" OK {
|
|
317
|
+
print(f" OK {label}: detected")
|
|
251
318
|
else:
|
|
252
|
-
print(f" FAIL {
|
|
319
|
+
print(f" FAIL {label}: NOT detected (found {sorted(got) or 'nothing'})")
|
|
253
320
|
failures += 1
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
321
|
+
for label, fname, body in CLEAN:
|
|
322
|
+
FINDINGS.clear()
|
|
323
|
+
with tempfile.TemporaryDirectory() as d:
|
|
324
|
+
with open(os.path.join(d, fname), "w", encoding="utf-8") as fh:
|
|
325
|
+
fh.write(body)
|
|
326
|
+
assert agentish(body), f"CLEAN FIXTURE NOT READ: {label} is not agent-related"
|
|
327
|
+
scan(d)
|
|
328
|
+
if FINDINGS:
|
|
329
|
+
print(f" FAIL {label}: produced {len(FINDINGS)} finding(s): "
|
|
330
|
+
f"{[f['check'] for f in FINDINGS]}")
|
|
331
|
+
failures += 1
|
|
332
|
+
else:
|
|
333
|
+
print(f" OK {label}: silent")
|
|
334
|
+
total = len(PLANTS) + len(CLEAN)
|
|
335
|
+
print(f"\nself-test: {total - failures}/{total} passed")
|
|
268
336
|
return 1 if failures else 0
|
|
269
337
|
|
|
270
338
|
|
|
@@ -5,13 +5,14 @@ description: >-
|
|
|
5
5
|
use, an AI pipeline — or when metering and billing the LLM access it burns. Covers tool-
|
|
6
6
|
calling loops, multi-stage pipelines with human checkpoints, provider routing with fallback
|
|
7
7
|
and retry, four-layer memory with confidence decay, context budgets, sub-agent coordination
|
|
8
|
-
and error hierarchies;
|
|
9
|
-
|
|
8
|
+
and error hierarchies; the work as a graph — parallel layers, fake edges, a checker before
|
|
9
|
+
a convergence; for resale: tiered wallets, the single markup boundary, two-phase commit
|
|
10
|
+
across a database and a provider API, spend-delta polling, budget and loop guardrails,
|
|
10
11
|
per-tenant key lifecycle. Triggers - "agent", "orchestrator", "tool calling", "sub-agent",
|
|
11
12
|
"LLM router", "fallback chain", "human in the loop", "memory layer", "LLM billing", "token
|
|
12
|
-
wallet", "агент", "оркестратор", "суб-агент", "роутер моделей", "человек в
|
|
13
|
-
памяти", "биллинг LLM", "
|
|
14
|
-
wording.
|
|
13
|
+
wallet", "checker node", "агент", "оркестратор", "суб-агент", "роутер моделей", "человек в
|
|
14
|
+
цикле", "слой памяти", "биллинг LLM", "граф задач". Not for a single LLM call in a script,
|
|
15
|
+
or for prompt wording.
|
|
15
16
|
---
|
|
16
17
|
|
|
17
18
|
# Agent Orchestrator — Production Best Practices
|
|
@@ -59,7 +60,7 @@ class AgentContext:
|
|
|
59
60
|
connection_config: ... | None # external resource config
|
|
60
61
|
user_id: str | None
|
|
61
62
|
preferred_provider: str | None # e.g. "openrouter"
|
|
62
|
-
model: str | None # e.g. "
|
|
63
|
+
model: str | None # e.g. "<provider>/<model-id>"
|
|
63
64
|
extra: dict[str, Any] # pipeline_action, flags, overrides
|
|
64
65
|
```
|
|
65
66
|
|
|
@@ -143,40 +144,28 @@ else:
|
|
|
143
144
|
|
|
144
145
|
## 3. Meta-Tools (Orchestrator-Level)
|
|
145
146
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
name="query_database",
|
|
151
|
-
description="Query the connected database. Handles SQL generation, validation, execution.",
|
|
152
|
-
parameters=[ToolParameter(name="question", type="string", description="Data question")]
|
|
153
|
-
)
|
|
154
|
-
ASK_USER_TOOL = Tool(
|
|
155
|
-
name="ask_user",
|
|
156
|
-
description="Ask the user a structured clarification question.",
|
|
157
|
-
parameters=[
|
|
158
|
-
ToolParameter(name="question", type="string", ...),
|
|
159
|
-
ToolParameter(name="question_type", type="string",
|
|
160
|
-
enum=["yes_no", "multiple_choice", "free_text"]),
|
|
161
|
-
ToolParameter(name="options", type="string", required=False),
|
|
162
|
-
]
|
|
163
|
-
)
|
|
164
|
-
```
|
|
165
|
-
|
|
166
|
-
**Assemble tools dynamically** based on available capabilities:
|
|
147
|
+
The orchestrator's tools **delegate to sub-agents** rather than execute:
|
|
148
|
+
`query_database` takes a question in natural language and the SQL agent behind it owns
|
|
149
|
+
generation, validation and execution. One parameter, one responsibility, and the caller
|
|
150
|
+
never learns the sub-agent exists.
|
|
167
151
|
|
|
168
152
|
```python
|
|
169
153
|
def get_tools(*, has_db=False, has_kb=False, has_mcp=False) -> list[Tool]:
|
|
154
|
+
"""Assembled per request from the same capability flags that build the prompt (§10)."""
|
|
170
155
|
tools = []
|
|
171
|
-
if has_db:
|
|
172
|
-
|
|
173
|
-
if
|
|
174
|
-
tools.append(SEARCH_CODEBASE)
|
|
175
|
-
if has_mcp:
|
|
176
|
-
tools.append(QUERY_MCP)
|
|
156
|
+
if has_db: tools.extend([QUERY_DB, PROCESS_DATA, MANAGE_RULES, ASK_USER])
|
|
157
|
+
if has_kb: tools.append(SEARCH_CODEBASE)
|
|
158
|
+
if has_mcp: tools.append(QUERY_MCP)
|
|
177
159
|
return tools
|
|
178
160
|
```
|
|
179
161
|
|
|
162
|
+
Two rules that are this layer's and not the prompt's: **a capability the request does not
|
|
163
|
+
have contributes no tool**, and every enum a tool accepts is closed at the schema
|
|
164
|
+
(`ask_user`'s `question_type` is `yes_no | multiple_choice | free_text`, never free
|
|
165
|
+
prose). How to *describe* a tool so the model picks the right one — the sentence naming
|
|
166
|
+
when to use it, and the neighbour it is confused with — is
|
|
167
|
+
`agent-harness/references/tools.md`.
|
|
168
|
+
|
|
180
169
|
---
|
|
181
170
|
|
|
182
171
|
## 4. Sub-Agent Retry and Validation
|
|
@@ -240,8 +229,8 @@ async def detect_complexity_adaptive(question, llm, history) -> bool:
|
|
|
240
229
|
### Pipeline Components
|
|
241
230
|
|
|
242
231
|
```
|
|
243
|
-
QueryPlanner → (single LLM call) → ExecutionPlan (
|
|
244
|
-
StageExecutor → runs stages
|
|
232
|
+
QueryPlanner → (single LLM call) → ExecutionPlan (stages + their depends_on)
|
|
233
|
+
StageExecutor → runs stages in DEPENDENCY LAYERS with validation + retry (§13)
|
|
245
234
|
StageValidator → checks data shape, row bounds, cross-stage consistency
|
|
246
235
|
StageContext → in-memory state (plan, results per stage, user feedback)
|
|
247
236
|
PipelineRun → DB-persisted state for resume/retry across requests
|
|
@@ -250,21 +239,27 @@ PipelineRun → DB-persisted state for resume/retry across requests
|
|
|
250
239
|
### Checkpoint Pattern (Human-in-the-Loop)
|
|
251
240
|
|
|
252
241
|
```python
|
|
253
|
-
for
|
|
254
|
-
|
|
255
|
-
validation = validator.validate(stage, result, stage_ctx)
|
|
242
|
+
for layer in plan.layers(): # Kahn over depends_on — never list order
|
|
243
|
+
results = await run_layer(layer, context) # execute_with_retries per stage, together
|
|
256
244
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
if
|
|
260
|
-
|
|
261
|
-
|
|
245
|
+
for i, (stage, result) in enumerate(zip(layer, results)):
|
|
246
|
+
validation = validator.validate(stage, result, stage_ctx)
|
|
247
|
+
if not validation.passed:
|
|
248
|
+
results[i] = await retry_failed_validation(stage, context, validation)
|
|
249
|
+
if results[i] is None:
|
|
250
|
+
return StageFailedResult(stage, validation) # ask user
|
|
262
251
|
|
|
263
|
-
|
|
252
|
+
if len(layer) > 1: # cheap per-stage checks ran first; this
|
|
253
|
+
verdict = checker.check(results) # one is the cross-item gate (§13)
|
|
254
|
+
if not verdict.passed:
|
|
255
|
+
return StageFailedResult(layer, verdict) # nothing converges on a flagged output
|
|
264
256
|
|
|
265
|
-
|
|
257
|
+
for stage, result in zip(layer, results):
|
|
258
|
+
stage_ctx.set_result(stage.id, result)
|
|
259
|
+
|
|
260
|
+
if any(s.checkpoint for s in layer):
|
|
266
261
|
persist_to_db(pipeline_run_id, stage_ctx)
|
|
267
|
-
return CheckpointResult(
|
|
262
|
+
return CheckpointResult(layer, results) # pause for user review
|
|
268
263
|
# User responds: "continue" | "modify" | "retry"
|
|
269
264
|
```
|
|
270
265
|
|
|
@@ -329,37 +324,22 @@ a floor.
|
|
|
329
324
|
mode cross a compaction boundary as copied typed blocks, not prose (§12).
|
|
330
325
|
## 8. Self-Learning Feedback Loops
|
|
331
326
|
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
After every SQL execution cycle, heuristic extractors analyze the attempt sequence:
|
|
335
|
-
|
|
336
|
-
| Extractor | Detects | Creates |
|
|
337
|
-
|-----------|---------|---------|
|
|
338
|
-
| Table preference | Wrong table A fixed to B | "Use `B` instead of `A`" |
|
|
339
|
-
| Column correction | column_not_found → suggested col | "Use `full_name` not `user_name`" |
|
|
340
|
-
| Format discovery | Division by 100/1000 added | "Amounts in cents, divide by 100" |
|
|
341
|
-
| Schema gotcha | `deleted_at IS NULL` added | "Soft-delete: filter active records" |
|
|
342
|
-
| Performance hint | Timeout fixed by LIMIT/date filter | "Always add LIMIT to this table" |
|
|
343
|
-
|
|
344
|
-
LLM-based deep analysis (3+ attempts, 1hr cooldown) for cross-query patterns.
|
|
345
|
-
|
|
346
|
-
### Cycle 2: User Feedback
|
|
327
|
+
Three cycles feed layers 3 and 4, and they differ by what supplies the signal:
|
|
347
328
|
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
# rejected → learning + note + flag stale benchmark
|
|
354
|
-
# Categorize rejection: currency/format → data_format, filter → schema_gotcha,
|
|
355
|
-
# table → table_preference, join → schema_gotcha
|
|
356
|
-
```
|
|
329
|
+
| Cycle | Signal | Produces |
|
|
330
|
+
|---|---|---|
|
|
331
|
+
| **Validation** | the attempt sequence of a call that failed and was then fixed | a learning, extracted by heuristic — the wrong table, a renamed column, a unit divisor, a soft-delete filter, a missing `LIMIT`. Deep LLM analysis only past 3 attempts, on a cooldown |
|
|
332
|
+
| **User feedback** | a thumbs-down, or a data verdict of confirmed / approximate / rejected | a benchmark, a session note with the deviation, or a learning plus a flag on the now-stale benchmark |
|
|
333
|
+
| **Lifecycle** | time, and contradiction | decay, conflict resolution by negation flip, and promotion of a pattern seen on two independent resources |
|
|
357
334
|
|
|
358
|
-
|
|
335
|
+
The extractors, the exact confidence arithmetic and the promotion query live in
|
|
336
|
+
`references/patterns.md` — **Learning Extraction Heuristics**, **Confidence Management**
|
|
337
|
+
and **Cross-Resource Learning Transfer** — and not here, because a decay rate is a
|
|
338
|
+
constant to tune and a constant with two homes is one that will disagree with itself.
|
|
359
339
|
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
340
|
+
**The rule the whole section exists for:** a learning is written from a **contrast** — the
|
|
341
|
+
attempt that failed beside the attempt that worked — never from a single successful run.
|
|
342
|
+
A system that learns from its successes learns its own habits.
|
|
363
343
|
|
|
364
344
|
---
|
|
365
345
|
|
|
@@ -383,39 +363,26 @@ class WorkflowTracker:
|
|
|
383
363
|
# orchestrator:sql_agent, orchestrator:llm_retry, orchestrator:warning
|
|
384
364
|
```
|
|
385
365
|
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
for i in range(0, len(text), chunk_size):
|
|
391
|
-
await tracker.emit(wf_id, "token", "streaming", text[i:i+chunk_size])
|
|
392
|
-
```
|
|
366
|
+
The final answer streams in chunks as `token` events on the same bus — a typing effect is
|
|
367
|
+
a chunked emit, not a second mechanism. What makes the feed reliable rather than decorative
|
|
368
|
+
is in `references/runtime.md`: a monotonic id per event so a reconnecting client can resume,
|
|
369
|
+
and the feed being a **view over the durable trace** rather than the record itself.
|
|
393
370
|
|
|
394
371
|
---
|
|
395
372
|
|
|
396
373
|
## 10. Dynamic System Prompts
|
|
397
374
|
|
|
398
|
-
|
|
375
|
+
**Assemble the prompt from the capabilities that are actually present**, in the same pass
|
|
376
|
+
that assembles the tools (§3): one section naming each live capability, the resource map
|
|
377
|
+
if there is one, the current learnings, then the guidelines. A prompt that describes a
|
|
378
|
+
tool the agent was not given is how a model spends a turn calling something that is not
|
|
379
|
+
there.
|
|
399
380
|
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
if has_connection:
|
|
406
|
-
sections.append("- query_database: ... SQL agent handles everything")
|
|
407
|
-
sections.append("- process_data: ... enrich/aggregate/filter")
|
|
408
|
-
sections.append("- manage_rules: ... CRUD project rules")
|
|
409
|
-
if has_kb:
|
|
410
|
-
sections.append("- search_codebase: ... RAG over indexed code")
|
|
411
|
-
|
|
412
|
-
if table_map:
|
|
413
|
-
sections.append(f"DATABASE TABLES: {table_map}")
|
|
414
|
-
if recent_learnings:
|
|
415
|
-
sections.append(recent_learnings) # "AGENT LEARNINGS: ..."
|
|
416
|
-
sections.append("GUIDELINES: ...") # routing rules, verification protocol
|
|
417
|
-
return "\n".join(sections)
|
|
418
|
-
```
|
|
381
|
+
What belongs in that text, at what altitude, and how to enumerate the vocabulary so the
|
|
382
|
+
agent stops inventing status values is the **`agent-harness`** skill's
|
|
383
|
+
`agent-harness/references/system-prompt.md` — one home, and it is not this one. What is *this* skill's
|
|
384
|
+
is the wiring: the prompt is rebuilt per request from the same capability flags the tool
|
|
385
|
+
list is built from, so the two can never disagree.
|
|
419
386
|
|
|
420
387
|
**Data Verification Protocol** (inject when DB connected):
|
|
421
388
|
- First-time metrics: ask user "Do these numbers match expectations?"
|
|
@@ -461,6 +428,33 @@ transcript.
|
|
|
461
428
|
|
|
462
429
|
---
|
|
463
430
|
|
|
431
|
+
## 13. The Work as a Graph
|
|
432
|
+
|
|
433
|
+
Before the loop, the pipeline or the sub-agents: **decide the shape.** A node is one unit
|
|
434
|
+
of work; an edge is a dependency, and an edge carries data. The full model, the source it
|
|
435
|
+
comes from, and what this host actually executes are in
|
|
436
|
+
[`references/graph-engineering.md`](references/graph-engineering.md).
|
|
437
|
+
|
|
438
|
+
Four rules, and these are the ones that change code:
|
|
439
|
+
|
|
440
|
+
- **Label every edge with what crosses it. No payload, no edge.** Run the fake-edge test
|
|
441
|
+
over any chain you inherited: write the steps as boxes, ask of each arrow whether data
|
|
442
|
+
from A actually enters B, and delete the arrows that only encode the order somebody
|
|
443
|
+
typed. Two or three per workflow is the normal yield.
|
|
444
|
+
- **`depends_on` is a claim, so execute by layer.** §5's executor walked `plan.stages` in
|
|
445
|
+
list order beside a model that declared its dependencies — which serialises a plan that
|
|
446
|
+
went to the trouble of saying it need not be. Kahn the graph; a cycle fails the plan
|
|
447
|
+
rather than deadlocking the run.
|
|
448
|
+
- **A parallel layer needs a checker before its convergence.** Three branches run, one
|
|
449
|
+
returns a hallucination, and the synthesis node cannot tell: it combines all three and
|
|
450
|
+
answers confidently. The checker decides *usable / not usable* and nothing else, and
|
|
451
|
+
the convergence depends on **the checker**, never directly on a branch.
|
|
452
|
+
- **Static unless you can name what forces dynamic.** A graph that picks its own next
|
|
453
|
+
nodes cannot be audited afterwards, because the shape that ran is not the shape anyone
|
|
454
|
+
drew. Where a run has to be explainable, that settles it.
|
|
455
|
+
|
|
456
|
+
---
|
|
457
|
+
|
|
464
458
|
## Checklist — Building a New Orchestrator
|
|
465
459
|
|
|
466
460
|
- [ ] Shared `AgentContext` dataclass with all sub-agents
|
|
@@ -485,6 +479,10 @@ transcript.
|
|
|
485
479
|
- [ ] `ask_user` clarification mechanism
|
|
486
480
|
- [ ] Graceful degradation (partial answers on context overflow or max iterations)
|
|
487
481
|
- [ ] Compaction ladder, tool-pair-safe boundaries, typed carryover, output offload
|
|
482
|
+
- [ ] Every declared dependency names the data it carries — the fake-edge test run once
|
|
483
|
+
- [ ] Plans executed in dependency layers, not in the order the stages were listed
|
|
484
|
+
- [ ] A checker between every parallel layer and the node that consumes it, and that
|
|
485
|
+
checker watched refusing a planted bad input at least once
|
|
488
486
|
|
|
489
487
|
---
|
|
490
488
|
|
|
@@ -496,6 +494,7 @@ stays an index and the two cannot drift apart.
|
|
|
496
494
|
|
|
497
495
|
| File | Read it when |
|
|
498
496
|
|---|---|
|
|
497
|
+
| [`references/graph-engineering.md`](references/graph-engineering.md) | you are deciding the **shape of the work** — the fake-edge test, the diamond, the checker node, static versus dynamic, and what the host actually runs |
|
|
499
498
|
| [`references/patterns.md`](references/patterns.md) | you need the **data models and algorithms** under the body |
|
|
500
499
|
| [`references/context-engineering.md`](references/context-engineering.md) | the loop is **running out of window** |
|
|
501
500
|
| [`references/runtime.md`](references/runtime.md) | the agent must **survive a crash, a pause, a second message or a schedule** |
|