@ssheleg/agent-stack 0.3.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 +90 -0
- package/LICENSE +21 -0
- package/README.md +107 -0
- package/SECURITY.md +63 -0
- package/bin/agent-stack.js +87 -0
- package/package.json +41 -0
- package/plugins/agent-stack/.claude-plugin/plugin.json +24 -0
- package/plugins/agent-stack/skills/agent-orchestrator/SKILL.md +473 -0
- package/plugins/agent-stack/skills/agent-orchestrator/references/llm-proxy-billing.md +255 -0
- package/plugins/agent-stack/skills/agent-orchestrator/references/patterns.md +365 -0
|
@@ -0,0 +1,473 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: agent-orchestrator
|
|
3
|
+
description: >-
|
|
4
|
+
Use when building an agent system — an orchestrator, an LLM-powered tool, a chatbot with tool
|
|
5
|
+
use, an AI pipeline — or when metering and billing the LLM access it burns. Covers tool-
|
|
6
|
+
calling loops, multi-stage pipelines with human checkpoints, provider routing with fallback
|
|
7
|
+
and retry, four-layer memory with confidence decay, context budgets, sub-agent coordination
|
|
8
|
+
and error hierarchies; for resale: tiered wallets, the single markup boundary, two-phase
|
|
9
|
+
commit across a database and a provider API, spend-delta polling, budget and loop guardrails,
|
|
10
|
+
per-tenant key lifecycle. Triggers - "agent", "orchestrator", "tool calling", "sub-agent",
|
|
11
|
+
"LLM router", "fallback chain", "human in the loop", "memory layer", "LLM billing", "token
|
|
12
|
+
wallet", "агент", "оркестратор", "суб-агент", "роутер моделей", "человек в цикле", "слой
|
|
13
|
+
памяти", "биллинг LLM", "лимит бюджета". Not for a single LLM call in a script, or for prompt
|
|
14
|
+
wording.
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
# Agent Orchestrator — Production Best Practices
|
|
18
|
+
|
|
19
|
+
Battle-tested patterns from a production multi-agent system. Apply these when building any agent
|
|
20
|
+
orchestrator, LLM-powered tool system, or agentic workflow.
|
|
21
|
+
|
|
22
|
+
## Architecture Overview
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
User Question
|
|
26
|
+
↓
|
|
27
|
+
ConversationalAgent (thin wrapper, backward-compat)
|
|
28
|
+
↓
|
|
29
|
+
OrchestratorAgent.run(AgentContext)
|
|
30
|
+
├─ Complexity check → simple (tool loop) or complex (pipeline)
|
|
31
|
+
├─ Context loading (parallel: staleness, MCP sources, KB check)
|
|
32
|
+
├─ History trimming
|
|
33
|
+
├─ Context budget allocation
|
|
34
|
+
├─ System prompt construction (dynamic, capability-aware)
|
|
35
|
+
└─ Execution:
|
|
36
|
+
├─ SIMPLE: iterative LLM tool-calling loop
|
|
37
|
+
│ LLM → tool calls → sub-agent dispatch → results → LLM → ... → final text
|
|
38
|
+
└─ COMPLEX: multi-stage pipeline
|
|
39
|
+
QueryPlanner → ExecutionPlan → StageExecutor → checkpoints → final
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## 1. The Orchestrator Pattern
|
|
45
|
+
|
|
46
|
+
### Shared Context Object
|
|
47
|
+
|
|
48
|
+
Pass a single immutable-ish context object to all sub-agents:
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
@dataclass
|
|
52
|
+
class AgentContext:
|
|
53
|
+
project_id: str
|
|
54
|
+
user_question: str
|
|
55
|
+
chat_history: list[Message]
|
|
56
|
+
llm_router: LLMRouter # provider abstraction with retry/fallback
|
|
57
|
+
tracker: WorkflowTracker # SSE event emitter for real-time UI
|
|
58
|
+
workflow_id: str # unique ID for this request
|
|
59
|
+
connection_config: ... | None # external resource config
|
|
60
|
+
user_id: str | None
|
|
61
|
+
preferred_provider: str | None # e.g. "openrouter"
|
|
62
|
+
model: str | None # e.g. "openai/gpt-4o"
|
|
63
|
+
extra: dict[str, Any] # pipeline_action, flags, overrides
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
**Key principles:**
|
|
67
|
+
- Sub-agents never modify context — they return typed results
|
|
68
|
+
- Provider/model preferences flow down from user → project defaults → app defaults
|
|
69
|
+
- `extra` dict carries pipeline state, flags like `_skip_complexity`, session IDs
|
|
70
|
+
|
|
71
|
+
### Sub-Agent Protocol
|
|
72
|
+
|
|
73
|
+
Every sub-agent extends a base class:
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
class BaseAgent(ABC):
|
|
77
|
+
@abstractmethod
|
|
78
|
+
async def run(self, context: AgentContext, **kwargs) -> AgentResult: ...
|
|
79
|
+
|
|
80
|
+
@property
|
|
81
|
+
@abstractmethod
|
|
82
|
+
def name(self) -> str: ...
|
|
83
|
+
|
|
84
|
+
@staticmethod
|
|
85
|
+
def accum_usage(total, usage): ... # merge token counters
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Typed result subclasses per agent (e.g. `SQLAgentResult` with `query`, `results`, `attempts`).
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## 2. Tool-Calling Loop (Simple Path)
|
|
93
|
+
|
|
94
|
+
The core agent loop pattern:
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
max_iter = settings.max_orchestrator_iterations # e.g. 10
|
|
98
|
+
for iteration in range(max_iter):
|
|
99
|
+
# 1. Context pressure management
|
|
100
|
+
messages, did_trim = trim_loop_messages(messages, context_window)
|
|
101
|
+
if should_wrap_up(messages, context_window):
|
|
102
|
+
messages.append(Message(role="system",
|
|
103
|
+
content="IMPORTANT: Stop making tool calls. Compose final answer now."))
|
|
104
|
+
|
|
105
|
+
# 2. LLM call with retry
|
|
106
|
+
llm_resp = await llm_call_with_retry(messages, tools, provider, model)
|
|
107
|
+
|
|
108
|
+
# 3. No tool calls = final answer
|
|
109
|
+
if not llm_resp.tool_calls:
|
|
110
|
+
final_text = llm_resp.content
|
|
111
|
+
break
|
|
112
|
+
|
|
113
|
+
# 4. Dispatch tool calls
|
|
114
|
+
messages.append(Message(role="assistant", content=llm_resp.content,
|
|
115
|
+
tool_calls=llm_resp.tool_calls))
|
|
116
|
+
|
|
117
|
+
# 5. Parallel execution (except sequential-only tools)
|
|
118
|
+
if len(llm_resp.tool_calls) > 1 and not has_sequential_tool:
|
|
119
|
+
results = await asyncio.gather(
|
|
120
|
+
*(handle_tool(tc, context) for tc in llm_resp.tool_calls),
|
|
121
|
+
return_exceptions=True)
|
|
122
|
+
else:
|
|
123
|
+
results = [await handle_tool(tc, context) for tc in llm_resp.tool_calls]
|
|
124
|
+
|
|
125
|
+
# 6. Append tool results
|
|
126
|
+
for tc, (text, sub_result) in zip(llm_resp.tool_calls, results):
|
|
127
|
+
messages.append(Message(role="tool", content=text,
|
|
128
|
+
tool_call_id=tc.id, name=tc.name))
|
|
129
|
+
else:
|
|
130
|
+
# Max iterations reached — compose partial answer from gathered data
|
|
131
|
+
final_text = "I reached maximum analysis steps. Here is what I found..."
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
**Critical details:**
|
|
135
|
+
- **Parallel tool dispatch**: Use `asyncio.gather` for independent tools, sequential for stateful ones (e.g. data processing that depends on prior query results)
|
|
136
|
+
- **Wrap-up injection**: At ~70% context capacity, inject a system message telling the LLM to stop making tools calls and give a final answer
|
|
137
|
+
- **In-loop trimming**: At ~80% capacity, collapse older assistant+tool pairs into one-liner summaries
|
|
138
|
+
- **Token limit recovery**: On `LLMTokenLimitError`, compress to 60% and retry once. If still fails, return partial answer
|
|
139
|
+
- **Max iterations guard**: Always have a hard limit. On exhaustion, compose best-effort answer from data gathered so far
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
## 3. Meta-Tools (Orchestrator-Level)
|
|
144
|
+
|
|
145
|
+
Define tools that **delegate to sub-agents**, not execute directly:
|
|
146
|
+
|
|
147
|
+
```python
|
|
148
|
+
QUERY_DATABASE_TOOL = Tool(
|
|
149
|
+
name="query_database",
|
|
150
|
+
description="Query the connected database. Handles SQL generation, validation, execution.",
|
|
151
|
+
parameters=[ToolParameter(name="question", type="string", description="Data question")]
|
|
152
|
+
)
|
|
153
|
+
ASK_USER_TOOL = Tool(
|
|
154
|
+
name="ask_user",
|
|
155
|
+
description="Ask the user a structured clarification question.",
|
|
156
|
+
parameters=[
|
|
157
|
+
ToolParameter(name="question", type="string", ...),
|
|
158
|
+
ToolParameter(name="question_type", type="string",
|
|
159
|
+
enum=["yes_no", "multiple_choice", "free_text"]),
|
|
160
|
+
ToolParameter(name="options", type="string", required=False),
|
|
161
|
+
]
|
|
162
|
+
)
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
**Assemble tools dynamically** based on available capabilities:
|
|
166
|
+
|
|
167
|
+
```python
|
|
168
|
+
def get_tools(*, has_db=False, has_kb=False, has_mcp=False) -> list[Tool]:
|
|
169
|
+
tools = []
|
|
170
|
+
if has_db:
|
|
171
|
+
tools.extend([QUERY_DB, PROCESS_DATA, MANAGE_RULES, ASK_USER])
|
|
172
|
+
if has_kb:
|
|
173
|
+
tools.append(SEARCH_CODEBASE)
|
|
174
|
+
if has_mcp:
|
|
175
|
+
tools.append(QUERY_MCP)
|
|
176
|
+
return tools
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
---
|
|
180
|
+
|
|
181
|
+
## 4. Sub-Agent Retry and Validation
|
|
182
|
+
|
|
183
|
+
Wrap every sub-agent call in retry + validation:
|
|
184
|
+
|
|
185
|
+
```python
|
|
186
|
+
MAX_SUB_AGENT_RETRIES = 2
|
|
187
|
+
|
|
188
|
+
for attempt in range(MAX_SUB_AGENT_RETRIES + 1):
|
|
189
|
+
try:
|
|
190
|
+
result = await sub_agent.run(context, question=q)
|
|
191
|
+
validation = validator.validate(result)
|
|
192
|
+
if validation.passed or attempt == MAX_SUB_AGENT_RETRIES:
|
|
193
|
+
return format_for_llm(result, validation.warnings), result
|
|
194
|
+
continue # retry on validation failure
|
|
195
|
+
except AgentRetryableError:
|
|
196
|
+
if attempt < MAX_SUB_AGENT_RETRIES: continue
|
|
197
|
+
return "Failed after retries", None
|
|
198
|
+
except AgentFatalError as e:
|
|
199
|
+
return f"Fatal: {e}", None # no retry
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
**Error hierarchy:**
|
|
203
|
+
```
|
|
204
|
+
AgentError (base)
|
|
205
|
+
├── AgentRetryableError → orchestrator retries with adjusted context
|
|
206
|
+
├── AgentFatalError → unrecoverable (bad config, auth failure)
|
|
207
|
+
├── AgentTimeoutError → retry with smaller context
|
|
208
|
+
└── AgentValidationError → sub-agent result failed quality checks
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
**Result validation** (check before returning to user):
|
|
212
|
+
- SQL: query present? execution error? zero rows (warn)? slow query >30s (warn)?
|
|
213
|
+
- Viz: valid chart type? appropriate for data shape? (pie with 100 slices → bar)
|
|
214
|
+
- Knowledge: non-empty answer? source citations present?
|
|
215
|
+
|
|
216
|
+
---
|
|
217
|
+
|
|
218
|
+
## 5. Multi-Stage Pipeline (Complex Path)
|
|
219
|
+
|
|
220
|
+
For complex queries requiring multiple data steps:
|
|
221
|
+
|
|
222
|
+
### Complexity Detection
|
|
223
|
+
|
|
224
|
+
Two-tier: fast heuristic + optional LLM check.
|
|
225
|
+
|
|
226
|
+
```python
|
|
227
|
+
COMPLEXITY_KEYWORDS = ["summary table", "pivot", "cross-reference", "compare",
|
|
228
|
+
"for each", "step 1", "first find", "then"]
|
|
229
|
+
|
|
230
|
+
def detect_complexity(question, history) -> bool:
|
|
231
|
+
return any(kw in question.lower() for kw in COMPLEXITY_KEYWORDS)
|
|
232
|
+
|
|
233
|
+
async def detect_complexity_adaptive(question, llm, history) -> bool:
|
|
234
|
+
# Lightweight LLM call: "Is this simple or complex? Reply 'simple' or 'complex'."
|
|
235
|
+
resp = await llm.complete([...], max_tokens=10)
|
|
236
|
+
return "complex" in resp.content.lower()
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
### Pipeline Components
|
|
240
|
+
|
|
241
|
+
```
|
|
242
|
+
QueryPlanner → (single LLM call) → ExecutionPlan (ordered stages)
|
|
243
|
+
StageExecutor → runs stages sequentially with validation + retry
|
|
244
|
+
StageValidator → checks data shape, row bounds, cross-stage consistency
|
|
245
|
+
StageContext → in-memory state (plan, results per stage, user feedback)
|
|
246
|
+
PipelineRun → DB-persisted state for resume/retry across requests
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
### Checkpoint Pattern (Human-in-the-Loop)
|
|
250
|
+
|
|
251
|
+
```python
|
|
252
|
+
for idx, stage in enumerate(plan.stages):
|
|
253
|
+
result = await execute_with_retries(stage, context)
|
|
254
|
+
validation = validator.validate(stage, result, stage_ctx)
|
|
255
|
+
|
|
256
|
+
if not validation.passed:
|
|
257
|
+
retried = await retry_failed_validation(stage, context, validation)
|
|
258
|
+
if retried is None:
|
|
259
|
+
return StageFailedResult(stage, validation) # ask user
|
|
260
|
+
result = retried
|
|
261
|
+
|
|
262
|
+
stage_ctx.set_result(stage.id, result)
|
|
263
|
+
|
|
264
|
+
if stage.checkpoint:
|
|
265
|
+
persist_to_db(pipeline_run_id, stage_ctx)
|
|
266
|
+
return CheckpointResult(stage, result) # pause for user review
|
|
267
|
+
# User responds: "continue" | "modify" | "retry"
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
### Pipeline Resume
|
|
271
|
+
|
|
272
|
+
```python
|
|
273
|
+
async def resume_pipeline(resume_info, context):
|
|
274
|
+
pipeline_run = load_from_db(resume_info["pipeline_run_id"])
|
|
275
|
+
plan = ExecutionPlan.from_json(pipeline_run.plan_json)
|
|
276
|
+
stage_ctx = StageContext.from_persistence(...)
|
|
277
|
+
|
|
278
|
+
resume_from = current_idx + 1 if action == "continue" else current_idx
|
|
279
|
+
return await executor.execute(plan, context, resume_from=resume_from, stage_ctx=stage_ctx)
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
---
|
|
283
|
+
|
|
284
|
+
## 6. LLM Provider Routing
|
|
285
|
+
|
|
286
|
+
A router in front of the providers, not a provider client in front of the app:
|
|
287
|
+
attempt in order, fall through on failure, and surface one error hierarchy
|
|
288
|
+
upward so the caller cannot tell which vendor answered.
|
|
289
|
+
|
|
290
|
+
Read `references/llm-proxy-billing.md` → **Model routing and fallbacks** for the
|
|
291
|
+
fallback chain and per-provider retry with exponential backoff, and its
|
|
292
|
+
**Guardrails** section for budgets, loop detection and auto-pause.
|
|
293
|
+
|
|
294
|
+
**Three traps that cost real money:**
|
|
295
|
+
|
|
296
|
+
- **A retry loop and a fallback chain multiply.** Three providers with three
|
|
297
|
+
retries each is nine calls for one prompt; cap the total attempts, not the
|
|
298
|
+
per-provider ones.
|
|
299
|
+
- **Health checks that only run on failure never recover.** A provider marked
|
|
300
|
+
unhealthy needs a scheduled probe, or the chain permanently runs one provider
|
|
301
|
+
short and nobody sees it — the requests still succeed.
|
|
302
|
+
- **Model selection has three levels** — the request, the tenant, the system
|
|
303
|
+
default — and a tenant override that silently loses to a request parameter is
|
|
304
|
+
how a cheap model ends up billed at a premium one's rate.
|
|
305
|
+
## 7. Multi-Layer Memory System
|
|
306
|
+
|
|
307
|
+
Four layers, each with a different lifetime and a different reason to exist:
|
|
308
|
+
|
|
309
|
+
| Layer | Scope | Lives | Holds |
|
|
310
|
+
|---|---|---|---|
|
|
311
|
+
| 1 Chat history | per session | minutes | the turns, trimmed to a token budget |
|
|
312
|
+
| 2 Working memory | per resource | days | what this task has established so far |
|
|
313
|
+
| 3 Long-term learnings | per resource | months | what worked, with a confidence score |
|
|
314
|
+
| 4 Insights | per project | permanent | conclusions that outlived their resource |
|
|
315
|
+
|
|
316
|
+
Read `references/patterns.md` for the data models, **Confidence Management**
|
|
317
|
+
(how a learning decays and when it is retired), **Learning Extraction
|
|
318
|
+
Heuristics**, **Fuzzy Deduplication** and **Conflict Resolution** — the four
|
|
319
|
+
mechanisms that decide what actually enters layers 3 and 4.
|
|
320
|
+
|
|
321
|
+
**The trap is the budget, not the storage.** Every layer competes for the same
|
|
322
|
+
context window, so allocation has to be decided per call rather than per layer:
|
|
323
|
+
a session that trims chat history to fit a large set of learnings has quietly
|
|
324
|
+
chosen old generalities over what the user said sixty seconds ago. Give layer 1
|
|
325
|
+
a floor.
|
|
326
|
+
## 8. Self-Learning Feedback Loops
|
|
327
|
+
|
|
328
|
+
### Cycle 1: Automatic (Validation Loop)
|
|
329
|
+
|
|
330
|
+
After every SQL execution cycle, heuristic extractors analyze the attempt sequence:
|
|
331
|
+
|
|
332
|
+
| Extractor | Detects | Creates |
|
|
333
|
+
|-----------|---------|---------|
|
|
334
|
+
| Table preference | Wrong table A fixed to B | "Use `B` instead of `A`" |
|
|
335
|
+
| Column correction | column_not_found → suggested col | "Use `full_name` not `user_name`" |
|
|
336
|
+
| Format discovery | Division by 100/1000 added | "Amounts in cents, divide by 100" |
|
|
337
|
+
| Schema gotcha | `deleted_at IS NULL` added | "Soft-delete: filter active records" |
|
|
338
|
+
| Performance hint | Timeout fixed by LIMIT/date filter | "Always add LIMIT to this table" |
|
|
339
|
+
|
|
340
|
+
LLM-based deep analysis (3+ attempts, 1hr cooldown) for cross-query patterns.
|
|
341
|
+
|
|
342
|
+
### Cycle 2: User Feedback
|
|
343
|
+
|
|
344
|
+
```python
|
|
345
|
+
# Thumbs down → analyze_negative_feedback() → learning
|
|
346
|
+
# Data validation:
|
|
347
|
+
# confirmed → store benchmark
|
|
348
|
+
# approximate → benchmark + session note (deviation details)
|
|
349
|
+
# rejected → learning + note + flag stale benchmark
|
|
350
|
+
# Categorize rejection: currency/format → data_format, filter → schema_gotcha,
|
|
351
|
+
# table → table_preference, join → schema_gotcha
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
### Cycle 3: Knowledge Lifecycle
|
|
355
|
+
|
|
356
|
+
- **Decay**: stale learnings -0.02/month, notes -0.1/60 days, insights -0.05/30 days
|
|
357
|
+
- **Conflict resolution**: negation flips deactivate old conflicting lessons
|
|
358
|
+
- **Global promotion**: patterns on 2+ resources promoted project-wide
|
|
359
|
+
|
|
360
|
+
---
|
|
361
|
+
|
|
362
|
+
## 9. Observability (SSE Event Streaming)
|
|
363
|
+
|
|
364
|
+
Real-time progress via `WorkflowTracker`:
|
|
365
|
+
|
|
366
|
+
```python
|
|
367
|
+
class WorkflowTracker:
|
|
368
|
+
# In-memory event bus with asyncio.Queue subscribers
|
|
369
|
+
async def begin(pipeline, context) -> workflow_id
|
|
370
|
+
async def emit(wf_id, step, status, detail)
|
|
371
|
+
async def end(wf_id, agent, status, detail)
|
|
372
|
+
|
|
373
|
+
@asynccontextmanager
|
|
374
|
+
async def step(wf_id, step_name, description):
|
|
375
|
+
# Emits started/completed/failed with elapsed_ms
|
|
376
|
+
|
|
377
|
+
# Event types:
|
|
378
|
+
# pipeline_start/end, thinking, token (streaming), orchestrator:llm_call,
|
|
379
|
+
# orchestrator:sql_agent, orchestrator:llm_retry, orchestrator:warning
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
Stream final answer text in chunks for typing effect:
|
|
383
|
+
|
|
384
|
+
```python
|
|
385
|
+
async def stream_tokens(wf_id, text, chunk_size=12):
|
|
386
|
+
for i in range(0, len(text), chunk_size):
|
|
387
|
+
await tracker.emit(wf_id, "token", "streaming", text[i:i+chunk_size])
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
---
|
|
391
|
+
|
|
392
|
+
## 10. Dynamic System Prompts
|
|
393
|
+
|
|
394
|
+
Build system prompts dynamically based on available capabilities:
|
|
395
|
+
|
|
396
|
+
```python
|
|
397
|
+
def build_system_prompt(*, project_name, db_type, has_connection, has_kb, table_map,
|
|
398
|
+
project_overview, recent_learnings):
|
|
399
|
+
sections = [f"You are an AI data assistant for '{project_name}'."]
|
|
400
|
+
sections.append("AVAILABLE CAPABILITIES:")
|
|
401
|
+
if has_connection:
|
|
402
|
+
sections.append("- query_database: ... SQL agent handles everything")
|
|
403
|
+
sections.append("- process_data: ... enrich/aggregate/filter")
|
|
404
|
+
sections.append("- manage_rules: ... CRUD project rules")
|
|
405
|
+
if has_kb:
|
|
406
|
+
sections.append("- search_codebase: ... RAG over indexed code")
|
|
407
|
+
|
|
408
|
+
if table_map:
|
|
409
|
+
sections.append(f"DATABASE TABLES: {table_map}")
|
|
410
|
+
if recent_learnings:
|
|
411
|
+
sections.append(recent_learnings) # "AGENT LEARNINGS: ..."
|
|
412
|
+
sections.append("GUIDELINES: ...") # routing rules, verification protocol
|
|
413
|
+
return "\n".join(sections)
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
**Data Verification Protocol** (inject when DB connected):
|
|
417
|
+
- First-time metrics: ask user "Do these numbers match expectations?"
|
|
418
|
+
- Financial figures: mention units (cents vs dollars), ask for confirmation
|
|
419
|
+
- Anomalies: proactively explain and ask user to verify
|
|
420
|
+
- Rejected data: investigate discrepancy, record finding as learning
|
|
421
|
+
|
|
422
|
+
---
|
|
423
|
+
|
|
424
|
+
## 11. Clarification Requests (ask_user)
|
|
425
|
+
|
|
426
|
+
Interrupt the tool loop to ask the user:
|
|
427
|
+
|
|
428
|
+
```python
|
|
429
|
+
async def handle_ask_user(tc, context, wf_id):
|
|
430
|
+
payload = {"question": ..., "question_type": "multiple_choice",
|
|
431
|
+
"options": [...], "context": "why I'm asking"}
|
|
432
|
+
raise _ClarificationRequestError(json.dumps(payload))
|
|
433
|
+
# Caught in orchestrator.run() → returns AgentResponse(response_type="clarification_request")
|
|
434
|
+
# Frontend renders special UI, user responds, next message continues flow
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
---
|
|
438
|
+
|
|
439
|
+
## Checklist — Building a New Orchestrator
|
|
440
|
+
|
|
441
|
+
- [ ] Shared `AgentContext` dataclass with all sub-agents
|
|
442
|
+
- [ ] `BaseAgent` protocol with typed results + `accum_usage()`
|
|
443
|
+
- [ ] Tool-calling loop with max iterations guard
|
|
444
|
+
- [ ] In-loop context trimming (80% compress, 70% wrap-up)
|
|
445
|
+
- [ ] Parallel tool dispatch where independent, sequential where stateful
|
|
446
|
+
- [ ] Sub-agent retry with validation (retryable vs fatal errors)
|
|
447
|
+
- [ ] Multi-provider LLM router with fallback chain + health checks
|
|
448
|
+
- [ ] Per-provider retry with exponential backoff (respect `retry_after`)
|
|
449
|
+
- [ ] Unified LLM error hierarchy with `user_message` property
|
|
450
|
+
- [ ] Context budget manager (priority-based allocation)
|
|
451
|
+
- [ ] Dynamic system prompt (capability-aware, learning-injected)
|
|
452
|
+
- [ ] Chat history trimming (tool condensing, LLM summarization)
|
|
453
|
+
- [ ] Working memory (session notes, fuzzy dedup, confidence decay)
|
|
454
|
+
- [ ] Long-term learnings (heuristic extraction, conflict resolution, global patterns)
|
|
455
|
+
- [ ] Insight memory (lifecycle, trust scoring, decay)
|
|
456
|
+
- [ ] Feedback pipeline (thumbs, data validation → learnings/notes/benchmarks)
|
|
457
|
+
- [ ] SSE event streaming for real-time progress
|
|
458
|
+
- [ ] Complexity detection (heuristic + adaptive LLM)
|
|
459
|
+
- [ ] Multi-stage pipeline with checkpoints and resume
|
|
460
|
+
- [ ] `ask_user` clarification mechanism
|
|
461
|
+
- [ ] Graceful degradation (partial answers on context overflow or max iterations)
|
|
462
|
+
|
|
463
|
+
---
|
|
464
|
+
|
|
465
|
+
## References
|
|
466
|
+
|
|
467
|
+
Load these when the task reaches them — the checklist above is the map, these
|
|
468
|
+
are the territory.
|
|
469
|
+
|
|
470
|
+
| File | Read it when |
|
|
471
|
+
|---|---|
|
|
472
|
+
| [`references/patterns.md`](references/patterns.md) | you need the **data models and algorithms**: message and result protocols, pipeline models, the SQL validation loop, context-window sizes and token estimation, learning-extraction heuristics, confidence lifecycle, fuzzy dedup, conflict resolution, cross-resource transfer, the no-LLM suggestion engine |
|
|
473
|
+
| [`references/llm-proxy-billing.md`](references/llm-proxy-billing.md) | the product **resells LLM access**: tiered wallets and where markup applies, two-phase commit against a provider API with compensating transactions, advisory locking, optimistic concurrency for reclaims, spend-delta polling and its three cases, budget/loop/auto-pause guardrails, per-tenant key lifecycle and healing, the refund waterfall, model routing |
|