Skip to content

Commit 2e2bc8d

Browse files
author
Dariusz Debowczyk
committed
Agents cleanup cont
1 parent 5386731 commit 2e2bc8d

File tree

248 files changed

+4168
-4688
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

248 files changed

+4168
-4688
lines changed

.beads/.local_version

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
0.49.0
1+
0.49.3

.beads/bd.sock.startlock

Lines changed: 0 additions & 1 deletion
This file was deleted.

.beads/issues.jsonl

Lines changed: 57 additions & 14 deletions
Large diffs are not rendered by default.

AGENTS.md

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,78 @@ Each package in `packages/` may contain:
2424
- Avoid exceptions for control flow - do not wrap everything in try/catch; either let exceptions bubble up or use monadic Cognesy\Utils\Result\Result for error handling if error needs to be handled on the same level
2525
- Use namespaces and PSR-4 autoloading
2626

27+
## Agents Package Style
28+
29+
- Data classes are `readonly final class` — all properties readonly, all mutations return new instances via `with*()` methods
30+
- Interfaces use `Can-` prefix (`CanUseTools`, `CanEmitAgentEvents`, `CanInterceptAgentLifecycle`)
31+
- Hooks use `Hook` suffix (`StepsLimitHook`, `ErrorPolicyHook`)
32+
- Accessors have no prefix: `state()`, `execution()`, `currentStep()` — not `getState()`
33+
- Mutators use `with` prefix and return `self`: `withState()`, `withCurrentStep()`
34+
- Named constructors for defaults: `::empty()`, `::fresh()`; for hydration: `::fromArray()`
35+
- Minimal docblocks — only on class/interface declarations; methods are self-documenting via types and naming
36+
- Method order: constructors → lifecycle/transitions → accessors → mutators → private helpers → serialization (`toArray`/`fromArray`)
37+
- Enums are string-backed for serialization; put logic (priority, comparison) as methods on the enum itself
38+
- Use `match(true) { ... }` for multi-condition branching
39+
- Use `$this->field ?? Default::empty()` pattern ("ensure pattern") to provide defaults instead of null-checking
40+
2741
# Design Principles
2842

2943
- Always start with extremely simple code, refactor when it is required - do not over-engineer solutions (YAGNI)
3044
- Use DDD (Domain Driven Design) principles - aggregate roots, value objects, entities, repositories, services
3145
- Use Clean Code and SOLID principles
3246
- Use interfaces for contracts, avoid concrete class dependencies
33-
- Early returns on errors, use monadic Cognesy\Utils\Result\Result for any complex error handling
47+
- Prefer using monadic designs for complex fragments of code - e.g. to avoid null checks and make the code cleaner and simpler.
48+
49+
## Agents Package Architecture
50+
51+
### Mental Model
52+
53+
- **AgentLoop** is a step-based iterator: each step = one LLM inference + tool execution cycle
54+
- Loop lifecycle: `beforeExecution`[`beforeStep``handleToolUse``afterStep` → check `shouldStop()`]*`afterExecution`
55+
- The loop yields intermediate states via `iterate()` (generator pattern) — callers can observe/persist between steps
56+
57+
### State Layering
58+
59+
- **AgentState** = session-level (persistent across executions): agentId, messages, metadata, optional `ExecutionState`
60+
- **ExecutionState** = transient per-run: executionId, status, steps, continuation signals — null between executions
61+
- **AgentStep** = immutable snapshot of one step: input/output messages, inference response, tool executions, errors
62+
- **StepExecution** = wraps AgentStep with timing and continuation metadata — keeps step data immutable while tracking execution details
63+
- State updates are always atomic — `withCurrentStepCompleted()` chains through the layers to prevent partial/inconsistent states
64+
- `AgentState.forNextExecution()` clears execution data while preserving session state
65+
66+
### Stop/Continuation
67+
68+
- **StopSignal** = value object with reason (enum) + message + context + source
69+
- **StopReason** enum has priority ordering for conflict resolution (ErrorForbade > StopRequested > StepsLimitReached > ...)
70+
- **ExecutionContinuation** aggregates stop signals; `shouldStop()` = has signals AND no continuation override
71+
- **AgentStopException** is a control-flow exception (not an error) — caught in loop, converted to StopSignal
72+
73+
### Invariant vs Variant Behavior
74+
75+
The boundary between `\Core` and `\Hook` is **whether the behavior is optional**:
76+
77+
- **Core state transitions** = invariant. Always happens, can't be removed, not configurable. Folded into `with*()` methods on state objects. Examples: `withCurrentStep()` routes step output to the correct message section; `forNextExecution()` clears the execution buffer; `withCurrentStepCompleted()` archives the step.
78+
- **Hooks** = variant. Configurable, optional, composable via builder. Examples: step limits, token limits, summarization, finish reason stopping. If you could imagine an agent that doesn't need it, it's a hook.
79+
80+
**State transitions must be complete.** Every `with*()` call leaves the state fully consistent — no follow-up call required. If behavior always follows a transition, fold it into that transition. A separate "remember to also call X" method implies the behavior is optional; if it isn't optional, the separation is wrong.
81+
82+
**The litmus test:** if you're writing a hook that always runs, can't be removed, and has ordering dependencies with other hooks — it's a missing state transition, not a hook.
83+
84+
**AgentLoop is domain-agnostic.** It orchestrates lifecycle phases (beforeStep → handleToolUse → afterStep → shouldStop) but has zero knowledge of message sections, buffer routing, or output formats. Domain behavior lives in state objects (invariant) or hooks (variant), never in the loop.
85+
86+
### Hooks
87+
88+
- Hooks implement `CanInterceptAgentLifecycle` and receive/return `HookContext`
89+
- **HookContext** carries state + trigger type + tool data; hooks mutate context (block tools, add errors, emit stop signals)
90+
- Hook categories by convention: Guard hooks (priority 200, run first — check limits), Transform hooks (modify state), Block hooks (prevent tool execution), Decision hooks (policy-based retry/stop/ignore)
91+
- **HookStack** chains hooks in priority order (higher = earlier); immutable — `with()` returns new stack
92+
93+
### Composition
94+
95+
- AgentBuilder composes: Tools + Driver + EventEmitter + HookStack + ToolExecutor → AgentLoop
96+
- Many small interfaces (`CanUseTools`, `CanExecuteToolCalls`, `CanInterceptAgentLifecycle`, `CanEmitAgentEvents`) — one role each
97+
- Events are readonly DTOs dispatched via event bus; emitter is a thin wrapper creating event objects from state
98+
- Extend via hooks, not subclassing — AgentLoop is not designed for inheritance (test subclass `TestAgentLoop` is the sole exception)
3499

35100
# Tests and Quality
36101

docs-build/packages/addons/agent_hooks.mdx

Lines changed: 28 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ $agent = AgentBuilder::new()
4444
})
4545

4646
->build();
47-
// @doctest id="4a0b"
47+
// @doctest id="f7c2"
4848
```
4949

5050
## Lifecycle Events
@@ -104,7 +104,7 @@ HookOutcome::block('Dangerous command detected');
104104

105105
// Stop the entire agent execution
106106
HookOutcome::stop('Budget exceeded');
107-
// @doctest id="645c"
107+
// @doctest id="d629"
108108
```
109109

110110
### Semantic Differences
@@ -146,7 +146,7 @@ function afterTool(ToolHookContext $ctx): HookOutcome {
146146

147147
return HookOutcome::proceed();
148148
}
149-
// @doctest id="f8fe"
149+
// @doctest id="2c95"
150150
```
151151

152152
### StepHookContext
@@ -167,7 +167,7 @@ function onStep(StepHookContext $ctx): HookOutcome {
167167

168168
return HookOutcome::proceed();
169169
}
170-
// @doctest id="a615"
170+
// @doctest id="e6d9"
171171
```
172172

173173
### StopHookContext
@@ -187,7 +187,7 @@ function onStop(StopHookContext $ctx): HookOutcome {
187187

188188
return HookOutcome::proceed(); // Allow stop
189189
}
190-
// @doctest id="5567"
190+
// @doctest id="1add"
191191
```
192192

193193
### ExecutionHookContext
@@ -206,7 +206,7 @@ function onExecution(ExecutionHookContext $ctx): HookOutcome {
206206

207207
return HookOutcome::proceed();
208208
}
209-
// @doctest id="40b2"
209+
// @doctest id="7830"
210210
```
211211

212212
### FailureHookContext
@@ -223,7 +223,7 @@ function onFailure(FailureHookContext $ctx): HookOutcome {
223223

224224
return HookOutcome::proceed();
225225
}
226-
// @doctest id="f253"
226+
// @doctest id="96aa"
227227
```
228228

229229
## Matchers
@@ -247,7 +247,7 @@ new ToolNameMatcher('*'); // Match all
247247

248248
// Regex patterns
249249
new ToolNameMatcher('/^(read|write)_.+$/');
250-
// @doctest id="b908"
250+
// @doctest id="3fd1"
251251
```
252252

253253
### EventTypeMatcher
@@ -263,7 +263,7 @@ new EventTypeMatcher(HookEvent::PreToolUse);
263263

264264
// Multiple events
265265
new EventTypeMatcher(HookEvent::BeforeStep, HookEvent::AfterStep);
266-
// @doctest id="8e88"
266+
// @doctest id="da63"
267267
```
268268

269269
### CompositeMatcher
@@ -294,7 +294,7 @@ $matcher = CompositeMatcher::and(
294294
),
295295
new CallableMatcher(fn($ctx) => $ctx->state()->metadata()->get('safe_mode')),
296296
);
297-
// @doctest id="52e7"
297+
// @doctest id="4d34"
298298
```
299299

300300
### CallableMatcher
@@ -307,7 +307,7 @@ use Cognesy\Addons\Agent\Hooks\Matchers\CallableMatcher;
307307
$matcher = new CallableMatcher(function (HookContext $ctx): bool {
308308
return $ctx->state()->metadata()->get('priority') === 'high';
309309
});
310-
// @doctest id="fa02"
310+
// @doctest id="ff80"
311311
```
312312

313313
## Priority
@@ -324,7 +324,7 @@ $builder
324324

325325
// Logging hooks run last (low priority)
326326
->onAfterToolUse($logger, priority: -100);
327-
// @doctest id="4918"
327+
// @doctest id="ec88"
328328
```
329329

330330
**Priority Guidelines:**
@@ -352,7 +352,7 @@ $builder->onAfterToolUse(
352352
priority: int = 0,
353353
matcher: string|HookMatcher|null = null,
354354
);
355-
// @doctest id="e2d1"
355+
// @doctest id="32c4"
356356
```
357357

358358
### Step Hooks
@@ -367,7 +367,7 @@ $builder->onBeforeStep(
367367
$builder->onAfterStep(
368368
callback: callable, // (AgentState) -> AgentState
369369
);
370-
// @doctest id="fddf"
370+
// @doctest id="7c77"
371371
```
372372

373373
### Execution Hooks
@@ -384,7 +384,7 @@ $builder->onExecutionEnd(
384384
callback: callable, // (ExecutionHookContext) -> HookOutcome|void
385385
priority: int = 0,
386386
);
387-
// @doctest id="7f22"
387+
// @doctest id="6412"
388388
```
389389

390390
### Continuation Hooks
@@ -401,7 +401,7 @@ $builder->onSubagentStop(
401401
callback: callable, // (StopHookContext) -> HookOutcome|void
402402
priority: int = 0,
403403
);
404-
// @doctest id="7cdd"
404+
// @doctest id="3a0c"
405405
```
406406

407407
### Error Hooks
@@ -412,7 +412,7 @@ $builder->onAgentFailed(
412412
callback: callable, // (FailureHookContext) -> HookOutcome|void
413413
priority: int = 0,
414414
);
415-
// @doctest id="64d1"
415+
// @doctest id="700a"
416416
```
417417

418418
### Unified Registration
@@ -428,7 +428,7 @@ $builder->addHook(
428428
hook: new CallableHook($callback, $matcher),
429429
priority: 100,
430430
);
431-
// @doctest id="929b"
431+
// @doctest id="f22b"
432432
```
433433

434434
## Creating Custom Hooks
@@ -457,7 +457,7 @@ class RateLimitHook implements Hook
457457
return $next($context);
458458
}
459459
}
460-
// @doctest id="b5bd"
460+
// @doctest id="7cb4"
461461
```
462462

463463
## Using HookStack Directly
@@ -482,7 +482,7 @@ if ($outcome->isBlocked()) {
482482
} elseif ($outcome->isStopped()) {
483483
// Handle stopped
484484
}
485-
// @doctest id="f2f0"
485+
// @doctest id="1d82"
486486
```
487487

488488
## Backward Compatibility
@@ -498,7 +498,7 @@ use Cognesy\Addons\Agent\Hooks\Adapters\StateProcessorAdapter;
498498

499499
$processor = new YourStateProcessor();
500500
$hook = new StateProcessorAdapter($processor, position: 'after');
501-
// @doctest id="1638"
501+
// @doctest id="7351"
502502
```
503503

504504
### ContinuationCriteriaAdapter
@@ -508,7 +508,7 @@ use Cognesy\Addons\Agent\Hooks\Adapters\ContinuationCriteriaAdapter;
508508

509509
$criterion = new YourContinuationCriterion();
510510
$hook = new ContinuationCriteriaAdapter($criterion);
511-
// @doctest id="b2da"
511+
// @doctest id="7a88"
512512
```
513513

514514
## Common Patterns
@@ -533,7 +533,7 @@ $builder->onBeforeToolUse(
533533
priority: 100, // Run first
534534
matcher: 'bash',
535535
);
536-
// @doctest id="fb43"
536+
// @doctest id="4e5e"
537537
```
538538

539539
### Execution Timing
@@ -550,7 +550,7 @@ $builder
550550
$duration = microtime(true) - $started;
551551
$this->metrics->record('execution_duration', $duration);
552552
});
553-
// @doctest id="882e"
553+
// @doctest id="06f6"
554554
```
555555

556556
### Conditional Continuation
@@ -573,7 +573,7 @@ $builder->onStop(function (StopHookContext $ctx): HookOutcome {
573573

574574
return HookOutcome::proceed();
575575
});
576-
// @doctest id="f7b5"
576+
// @doctest id="aab6"
577577
```
578578

579579
### Error Recovery Logging
@@ -599,7 +599,7 @@ $builder->onAgentFailed(function (FailureHookContext $ctx): void {
599599
$this->alerting->sendCritical($exception);
600600
}
601601
});
602-
// @doctest id="0505"
602+
// @doctest id="68fb"
603603
```
604604

605605
## Architecture
@@ -631,7 +631,7 @@ $builder->onAgentFailed(function (FailureHookContext $ctx): void {
631631
│ │ execution│ │ action │ │ entirely │ │
632632
│ └──────────┘ └──────────┘ └──────────┘ │
633633
└─────────────────────────────────────────────────────────────┘
634-
// @doctest id="e86c"
634+
// @doctest id="2fab"
635635
```
636636

637637
## File Structure
@@ -672,5 +672,5 @@ packages/addons/src/Agent/Hooks/
672672
└── Adapters/
673673
├── StateProcessorAdapter.php
674674
└── ContinuationCriteriaAdapter.php
675-
// @doctest id="3f06"
675+
// @doctest id="b442"
676676
```

docs-build/packages/http/1-overview.mdx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ HttpClient
9494
└── withRequest() - Create pending request for execution
9595
└── pool() - Execute multiple requests concurrently
9696
└── withPool() - Create pending pool for deferred execution
97-
// @doctest id="d05c"
97+
// @doctest id="0504"
9898
```
9999

100100
### Middleware Layer
@@ -105,7 +105,7 @@ The middleware system allows for processing requests and responses through a cha
105105
Request -> Middleware 1 -> Middleware 2 -> ... -> Driver -> External API
106106
107107
Response <- Middleware 1 <- Middleware 2 <- ... <- Driver <- HTTP Response
108-
// @doctest id="7ebd"
108+
// @doctest id="5825"
109109
```
110110

111111
Key components:
@@ -123,7 +123,7 @@ CanHandleHttpRequest (interface)
123123
├── SymfonyDriver
124124
├── LaravelDriver
125125
└── MockHttpDriver (for testing)
126-
// @doctest id="07e0"
126+
// @doctest id="e4dd"
127127
```
128128

129129
### Adapter Layer
@@ -136,7 +136,7 @@ HttpResponse (interface)
136136
├── SymfonyHttpResponse
137137
├── LaravelHttpResponse
138138
└── MockHttpResponse
139-
// @doctest id="5438"
139+
// @doctest id="74e3"
140140
```
141141

142142
## Supported HTTP Clients

0 commit comments

Comments
 (0)