Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7f47ec85da | |||
| 21728618ca | |||
| 09bc5222a5 | |||
| c3ed77a95c | |||
| 00be1004f1 | |||
| 82502404cd | |||
| 980fcabc48 | |||
| c8a73a1397 | |||
| 35bf1d6962 | |||
| 905fa7f343 | |||
| 8170dabb4f |
+10
-4
@@ -6,6 +6,8 @@ Changed `wf10_batch.robot` to be less likely to create files, and
|
||||
`plan_generation_graph.robot` to give more test answers.
|
||||
|
||||
## [Unreleased]
|
||||
- **test(e2e): restore complete M2 acceptance test** (#11191): Restored the truncated M2 full actor compiler and LLM integration e2e acceptance test to its complete 10-step form. Added dynamic LLM provider selection via `Resolve LLM Actor` (falls back to Anthropic when OpenAI is unavailable or quota-exhausted), replacing hardcoded `gpt-4` / `openai/gpt-4` references in the actor config and action YAML. Added explicit return-code validation (`Should Be Equal As Integers ${r_actor.rc} 0`) for the actor registration step.
|
||||
- **docs(a2a): ACP to A2A migration guide** (#10230): Added migration guide documenting how to upgrade from the ACP module to the A2A module introduced in v3.6.0, including symbol renames, field renames, operation-name mappings, and YAML configuration updates.
|
||||
- **Plan Prompt JSON Timing Field** (#9353): `agents plan prompt --format json` now
|
||||
includes `timing.started` as an ISO 8601 UTC timestamp in the JSON envelope,
|
||||
matching the spec (§CLI Commands — `agents plan prompt`). Extended
|
||||
@@ -445,6 +447,10 @@ ensuring data is stored with proper parameter values.
|
||||
versions (<3.13.4) cannot be installed even if upstream transitive dependencies have loose
|
||||
version constraints.
|
||||
|
||||
### Changed
|
||||
|
||||
- **Timeline dashboard updated with 2026-04-18 progress snapshot** (#10288): Added schedule adherence tables and daily milestone snapshot for April 18, covering M3-M10 status including overdue milestones (M3-M7) and in-progress milestones (M8-M10). Risk assessment shows M6 (v3.5.0) highest risk with 1,130 open issues at 18.1% completion.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Concurrent ValidationPipeline stdout/stderr restoration** (#7623): Fixed a race
|
||||
@@ -966,8 +972,8 @@ uko-oo:Class` triple emission in `PythonAnalyzer._extract_class()` so that
|
||||
|
||||
- **ACMS Context CLI Commands** (`context show` / `context clear`) (#9586): Implemented
|
||||
two new CLI commands for the ACMS (Advanced Context Management System). `context show
|
||||
<view>` displays assembled context for a named view including per-tier budget utilization
|
||||
summary (hot tier tokens vs. token budget; warm/cold tiers fragments vs. decision budget). `context clear` removes context index
|
||||
<view>` displays assembled context with per-tier budget utilization summary (hot tier
|
||||
tokens vs. token budget; warm/cold tiers fragments vs. decision budget). `context clear` removes context index
|
||||
entries filtered by `--path`, `--tag`, or `--tier`; supports `--yes` flag to bypass
|
||||
interactive confirmation for non-interactive/CI use. Includes full `--help` documentation,
|
||||
input validation, proper error handling, Robot Framework integration tests, and ASV
|
||||
@@ -1172,8 +1178,8 @@ Documentation Report (Cycle N)` issues every 10 cycles (~3.3 hours). The manager
|
||||
context during plan execution. Added `context_tier_hydrator.py` that reads files from
|
||||
linked project resources (via `git ls-files` or `os.walk`) and stores them as
|
||||
`TieredFragment` objects in the tier service. Hydration runs automatically before context
|
||||
assembly in `LLMExecuteActor.execute()`. Respects max file size (256KB), total budget
|
||||
(10MB), binary file exclusion, and `.git`/`node_modules`/`__pycache__` directory
|
||||
assembly in `LLMExecuteActor.execute()`. Respects max file size (256 KB), total budget
|
||||
(10 MB), binary file exclusion, and `.git`/`node_modules`/`__pycache__` directory
|
||||
skipping. (#1028)
|
||||
|
||||
- **Product-Builder Tracking Migration**: `product-builder` now creates individual
|
||||
|
||||
@@ -87,3 +87,4 @@ Below are some of the specific details of various contributions.
|
||||
Below are some specific details of individual PR contributions.
|
||||
|
||||
* HAL 9000 has contributed the configurable merge strategy implementation (PR #9610 / issue #9559): three configurable merge strategies (prefer-parent, prefer-subplan, manual) for plan three-way merges, MergeStrategy StrEnum with helper methods, MergeStrategyService for conflict resolution, BDD test suite with 8 scenarios, and Robot Framework integration tests.
|
||||
* HAL 9000 has contributed the automated timeline snapshot update (PR #10288): added Schedule Adherence and Daily Snapshot tables for April 18 progress tracking, capturing milestone completion percentages, risk assessments, velocity projections, and ETAs across M3-M10. Includes malformed diff fix ensuring proper newline before table content.
|
||||
|
||||
@@ -134,3 +134,38 @@ config = ServerConnectionConfig(url="https://my-server.example.com", token="..."
|
||||
```
|
||||
|
||||
Validates URL format and token presence.
|
||||
|
||||
---
|
||||
|
||||
## ACP to A2A Rename (v3.6.0)
|
||||
|
||||
> **Breaking change in v3.6.0.** The `cleveragents.acp` module was renamed to
|
||||
> `cleveragents.a2a`. All `Acp*` class names were renamed to `A2a*`. The
|
||||
> `AcpRequest` and `AcpResponse` field names were updated to comply with the
|
||||
> JSON-RPC 2.0 specification.
|
||||
|
||||
For the complete migration guide, see
|
||||
[`docs/development/acp-to-a2a-migration.md`](../development/acp-to-a2a-migration.md).
|
||||
|
||||
**Symbol rename summary:**
|
||||
|
||||
| Old (ACP) | New (A2A) |
|
||||
|---|---|
|
||||
| `AcpRequest` | `A2aRequest` |
|
||||
| `AcpResponse` | `A2aResponse` |
|
||||
| `AcpLocalFacade` | `A2aLocalFacade` |
|
||||
| `AcpError` | `A2aError` |
|
||||
| `AcpEventQueue` | `A2aEventQueue` |
|
||||
|
||||
**Field rename summary (`A2aRequest` / `A2aResponse`):**
|
||||
|
||||
| Old Field | New Field |
|
||||
|---|---|
|
||||
| `a2a_version` | `jsonrpc` |
|
||||
| `request_id` | `id` |
|
||||
| `operation` | `method` |
|
||||
| `payload` | `params` |
|
||||
| `data` | `result` |
|
||||
| `error_detail` | `error` |
|
||||
|
||||
See [ADR-047](../adr/ADR-047-acp-standard-adoption.md) for architectural rationale.
|
||||
|
||||
@@ -0,0 +1,264 @@
|
||||
# ACP to A2A Module Rename and Symbol Standardization
|
||||
|
||||
> **Introduced in:** v3.6.0
|
||||
> **Architectural decision:** [ADR-047 - A2A Standard Adoption](../adr/ADR-047-acp-standard-adoption.md)
|
||||
> **Module path:** `cleveragents.a2a` (commit `449c33b7`)
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
In v3.6.0 the internal **ACP (Agent Client Protocol)** module was renamed to
|
||||
**A2A (Agent-to-Agent Protocol)** and all public symbols were standardized to
|
||||
align with the external [A2A open standard](https://a2a-protocol.org) (Linux
|
||||
Foundation, Apache 2.0).
|
||||
|
||||
This was not merely a rename - it was an architectural shift from a bespoke
|
||||
JSON envelope protocol to the industry-standard A2A wire format built on
|
||||
**JSON-RPC 2.0**. The change eliminates the proprietary ACP envelope, replaces
|
||||
custom REST routing with JSON-RPC 2.0 method dispatch, and introduces
|
||||
**Agent Card** discovery (`/.well-known/agent.json`).
|
||||
|
||||
---
|
||||
|
||||
## What Changed
|
||||
|
||||
### 1. Module Path
|
||||
|
||||
| Before (ACP) | After (A2A) |
|
||||
|---|---|
|
||||
| `cleveragents.acp` | `cleveragents.a2a` |
|
||||
| `from cleveragents.acp import ...` | `from cleveragents.a2a import ...` |
|
||||
|
||||
### 2. Class and Symbol Names
|
||||
|
||||
All public symbols were renamed from `Acp*` to `A2a*`:
|
||||
|
||||
| Old Symbol (ACP) | New Symbol (A2A) | Notes |
|
||||
|---|---|---|
|
||||
| `AcpRequest` | `A2aRequest` | Wire format changed - see section 3 |
|
||||
| `AcpResponse` | `A2aResponse` | Wire format changed - see section 3 |
|
||||
| `AcpError` | `A2aError` | Base exception |
|
||||
| `AcpNotAvailableError` | `A2aNotAvailableError` | Server-mode guard |
|
||||
| `AcpOperationNotFoundError` | `A2aOperationNotFoundError` | Unknown method |
|
||||
| `AcpVersionMismatchError` | `A2aVersionMismatchError` | Protocol version guard |
|
||||
| `AcpLocalFacade` | `A2aLocalFacade` | Local-mode dispatcher |
|
||||
| `AcpHttpTransport` | `A2aHttpTransport` | Server-mode transport stub |
|
||||
| `AcpEventQueue` | `A2aEventQueue` | In-process event delivery |
|
||||
| `AcpVersionNegotiator` | `A2aVersionNegotiator` | Version negotiation |
|
||||
| `AcpVersion` | `A2aVersion` | Version constants |
|
||||
| `AcpEvent` | `A2aEvent` | SSE event envelope |
|
||||
| `AcpErrorDetail` | `A2aErrorDetail` | JSON-RPC error object |
|
||||
|
||||
### 3. Request and Response Field Names
|
||||
|
||||
`A2aRequest` and `A2aResponse` now use **JSON-RPC 2.0** field names instead of
|
||||
the proprietary ACP envelope fields. These models are defined in
|
||||
`cleveragents.a2a.models` (commit `449c33b7`):
|
||||
|
||||
| Old Field (ACP) | New Field (A2A / JSON-RPC 2.0) | Type |
|
||||
|---|---|---|
|
||||
| `a2a_version` | `jsonrpc` (always `"2.0"`) | `str` |
|
||||
| `request_id` | `id` | `str` |
|
||||
| `operation` | `method` | `str` |
|
||||
| `payload` | `params` | `dict[str, Any]` |
|
||||
| `data` (success path) | `result` | `dict[str, Any] or None` |
|
||||
| `error_detail` | `error` | `A2aErrorDetail or None` |
|
||||
|
||||
**Before (ACP):**
|
||||
```python
|
||||
from cleveragents.acp import AcpRequest, AcpLocalFacade
|
||||
|
||||
request = AcpRequest(
|
||||
a2a_version="1.0",
|
||||
request_id="req-001",
|
||||
operation="plan.status",
|
||||
payload={"plan_id": "01HXRCF1..."},
|
||||
)
|
||||
response = facade.dispatch(request)
|
||||
if response.data:
|
||||
print(response.data["phase"])
|
||||
```
|
||||
|
||||
**After (A2A):**
|
||||
```python
|
||||
from cleveragents.a2a import A2aRequest, A2aLocalFacade
|
||||
|
||||
request = A2aRequest(
|
||||
jsonrpc="2.0",
|
||||
id="req-001",
|
||||
method="_cleveragents/plan/status",
|
||||
params={"plan_id": "01HXRCF1..."},
|
||||
)
|
||||
response = facade.dispatch(request)
|
||||
if response.result:
|
||||
print(response.result["phase"])
|
||||
```
|
||||
|
||||
### 4. Operation Names
|
||||
|
||||
The ACP proprietary operation names were replaced with the A2A standard
|
||||
`_cleveragents/` extension method namespace. The legacy names are still
|
||||
accepted by `A2aLocalFacade.dispatch()` in `cleveragents.a2a.facade`
|
||||
(commit `449c33b7`) for backward compatibility but are **deprecated** and
|
||||
will be removed in a future major version.
|
||||
|
||||
| Old Operation (ACP / deprecated) | New Operation (A2A) |
|
||||
|---|---|
|
||||
| `session.create` | `_cleveragents/session/create` (via `message/send`) |
|
||||
| `session.close` | `_cleveragents/session/close` |
|
||||
| `plan.create` | `_cleveragents/plan/use` |
|
||||
| `plan.execute` | `_cleveragents/plan/execute` |
|
||||
| `plan.status` | `_cleveragents/plan/status` |
|
||||
| `plan.diff` | `_cleveragents/plan/diff` |
|
||||
| `plan.apply` | `_cleveragents/plan/apply` |
|
||||
| `registry.list_tools` | `_cleveragents/registry/tool/list` |
|
||||
| `registry.list_resources` | `_cleveragents/registry/resource/list` |
|
||||
| `context.get` | `_cleveragents/context/show` |
|
||||
| `event.subscribe` | (event delivery via SSE `message/stream`) |
|
||||
|
||||
The full list of supported A2A extension methods is documented in
|
||||
[`docs/reference/a2a.md`](../reference/a2a.md#extension-methods).
|
||||
|
||||
### 5. YAML and Configuration Files
|
||||
|
||||
If you have YAML configuration files, actor definitions, or skill files that
|
||||
reference the old ACP module path or operation names, update them as follows:
|
||||
|
||||
**Actor YAML - protocol binding:**
|
||||
```yaml
|
||||
# Before (ACP)
|
||||
protocol:
|
||||
type: acp
|
||||
version: "1.0"
|
||||
|
||||
# After (A2A)
|
||||
protocol:
|
||||
type: a2a
|
||||
version: "2.0"
|
||||
```
|
||||
|
||||
**Skill YAML - operation references:**
|
||||
```yaml
|
||||
# Before (ACP)
|
||||
operations:
|
||||
- operation: plan.create
|
||||
- operation: plan.execute
|
||||
|
||||
# After (A2A)
|
||||
operations:
|
||||
- method: _cleveragents/plan/use
|
||||
- method: _cleveragents/plan/execute
|
||||
```
|
||||
|
||||
**Server connection config:**
|
||||
```yaml
|
||||
# Before (ACP)
|
||||
server:
|
||||
protocol: acp
|
||||
endpoint: https://my-server.example.com/acp
|
||||
|
||||
# After (A2A)
|
||||
server:
|
||||
protocol: a2a
|
||||
endpoint: https://my-server.example.com/a2a
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Migration Steps
|
||||
|
||||
Follow these steps when upgrading from a codebase that used the ACP module:
|
||||
|
||||
### Step 1 - Update imports
|
||||
|
||||
Replace all `cleveragents.acp` imports with `cleveragents.a2a`:
|
||||
|
||||
```bash
|
||||
# Find all files that import from the old ACP module
|
||||
grep -r "from cleveragents.acp" src/ tests/
|
||||
grep -r "import cleveragents.acp" src/ tests/
|
||||
```
|
||||
|
||||
Then update each import:
|
||||
```python
|
||||
# Before
|
||||
from cleveragents.acp import AcpLocalFacade, AcpRequest, AcpResponse
|
||||
|
||||
# After
|
||||
from cleveragents.a2a import A2aLocalFacade, A2aRequest, A2aResponse
|
||||
```
|
||||
|
||||
### Step 2 - Rename symbol references
|
||||
|
||||
Use your editor's find-and-replace to rename all `Acp*` symbols to `A2a*`
|
||||
(see the complete symbol table in section 2 above).
|
||||
|
||||
### Step 3 - Update request field names
|
||||
|
||||
Update all `AcpRequest` and `AcpResponse` constructor calls and field accesses
|
||||
to use the new JSON-RPC 2.0 field names (see section 3 above).
|
||||
|
||||
### Step 4 - Update operation names
|
||||
|
||||
Replace deprecated ACP operation strings with the new `_cleveragents/`
|
||||
extension method names (see section 4 above). The legacy names still work but
|
||||
emit deprecation warnings.
|
||||
|
||||
### Step 5 - Update YAML files
|
||||
|
||||
Search for any YAML files that reference `acp` in protocol type fields or
|
||||
operation names and update them (see section 5 above).
|
||||
|
||||
### Step 6 - Verify
|
||||
|
||||
Run the full test suite to confirm no regressions:
|
||||
|
||||
```bash
|
||||
nox -s unit_tests
|
||||
nox -s integration_tests
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Backward Compatibility
|
||||
|
||||
The `A2aLocalFacade.dispatch()` method in `cleveragents.a2a.facade.A2aLocalFacade`
|
||||
(commit `449c33b7`) continues to accept the legacy ACP operation names
|
||||
(`session.create`, `plan.create`, etc.) via the `_LEGACY_OPERATIONS` list.
|
||||
This backward compatibility shim will be removed in the next major version.
|
||||
|
||||
The `A2aVersion` class in `cleveragents.a2a.models.A2aVersion` (commit
|
||||
`449c33b7`) retains the `CURRENT` and `SUPPORTED` constants for backward
|
||||
compatibility, though the wire format now uses the `jsonrpc` field rather than
|
||||
a proprietary `a2a_version` field.
|
||||
|
||||
---
|
||||
|
||||
## Architectural Rationale
|
||||
|
||||
The rename was driven by the adoption of the external A2A open standard
|
||||
([a2a-protocol.org](https://a2a-protocol.org)), which supersedes the bespoke
|
||||
ACP protocol. Key reasons for the change:
|
||||
|
||||
- **Ecosystem alignment**: A2A is governed by the Linux Foundation with 22k+
|
||||
GitHub stars and SDKs in Python, Go, JavaScript, Java, and .NET.
|
||||
- **Agent Card discovery**: The standard introduces `/.well-known/agent.json`
|
||||
for dynamic capability advertisement.
|
||||
- **JSON-RPC 2.0**: Replaces the proprietary envelope with a mature, widely-
|
||||
tooled standard.
|
||||
- **Extensibility**: The `_cleveragents/` extension namespace cleanly separates
|
||||
platform-specific operations from standard agent interactions.
|
||||
|
||||
See [ADR-047](../adr/ADR-047-acp-standard-adoption.md) for the full
|
||||
architectural rationale and decision record.
|
||||
|
||||
---
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- [A2A Reference](../reference/a2a.md) - complete API reference for the `cleveragents.a2a` module
|
||||
- [A2A API](../api/a2a.md) - class-level API documentation
|
||||
- [ADR-026 - Agent-to-Agent Protocol](../adr/ADR-026-agent-client-protocol.md) - protocol boundary design
|
||||
- [ADR-047 - A2A Standard Adoption](../adr/ADR-047-acp-standard-adoption.md) - rationale for adopting the external standard
|
||||
- [ADR-048 - Server Application Architecture](../adr/ADR-048-server-application-architecture.md) - server implementation using A2A
|
||||
@@ -0,0 +1,34 @@
|
||||
# Milestones
|
||||
|
||||
This section documents the major milestones in the CleverAgents v3 release series.
|
||||
Each milestone page covers the features delivered, CLI commands introduced, and
|
||||
architectural decisions made during that release cycle.
|
||||
|
||||
## Release Overview
|
||||
|
||||
| Milestone | Version | Title | Status |
|
||||
|-----------|---------|-------|--------|
|
||||
| M1 | [v3.0.0](v3.0.0.md) | Minimal Local Source-Code Workflow | Released |
|
||||
| M2 | [v3.1.0](v3.1.0.md) | Actor Compiler + Full LLM Integration | Released |
|
||||
|
||||
## What Is a Milestone?
|
||||
|
||||
CleverAgents uses **Clever Semantic Versioning** (MAJOR.MINOR.PATCH) where each
|
||||
MINOR version bump corresponds to a planned milestone delivering a coherent set of
|
||||
features. Milestones are tracked as Forgejo milestones and linked to epics
|
||||
in the issue tracker.
|
||||
|
||||
Each milestone page documents:
|
||||
|
||||
- **Goals** — What the milestone set out to achieve
|
||||
- **Delivered Features** — Concrete capabilities added
|
||||
- **CLI Reference** — Commands introduced or changed
|
||||
- **Architecture Notes** — Key design decisions
|
||||
- **Migration Guide** — Breaking changes and upgrade steps (if any)
|
||||
|
||||
## Related Resources
|
||||
|
||||
- [Implementation Timeline](../timeline.md) — Full chronological timeline
|
||||
- [Architecture](../architecture.md) — System architecture overview
|
||||
- [API Reference](../api/index.md) — Module-level API documentation
|
||||
- [CHANGELOG](https://git.cleverthis.com/cleveragents/cleveragents-core/src/branch/master/CHANGELOG.md) — Detailed change log
|
||||
@@ -0,0 +1,142 @@
|
||||
# v3.0.0 — Action, Resource, Project & Plan CLI Reference
|
||||
|
||||
Complete command documentation for all CleverAgents v3.0.0 CLI commands.
|
||||
|
||||
---
|
||||
|
||||
## Action Management (`agents action`)
|
||||
|
||||
Actions are reusable plan templates defined in YAML. They specify which actors
|
||||
handle each phase of the plan lifecycle and what arguments the plan accepts.
|
||||
|
||||
### `agents action create`
|
||||
|
||||
```bash
|
||||
agents action create --config ./my-action.yaml
|
||||
```
|
||||
|
||||
**Minimal action YAML:**
|
||||
|
||||
```yaml
|
||||
name: local/code-coverage
|
||||
description: Increase code coverage to the target percentage
|
||||
strategy_actor: openai/gpt-4
|
||||
execution_actor: openai/gpt-4
|
||||
definition_of_done: |
|
||||
Line coverage reaches the target percentage for all modules under src/.
|
||||
arguments:
|
||||
- name: target_coverage
|
||||
type: integer
|
||||
required: true
|
||||
```
|
||||
|
||||
**Full action YAML:**
|
||||
|
||||
```yaml
|
||||
name: local/refactor
|
||||
description: Refactor a module to follow SOLID principles
|
||||
strategy_actor: openai/gpt-4
|
||||
execution_actor: anthropic/claude-3
|
||||
estimation_actor: openai/gpt-4
|
||||
invariant_actor: anthropic/claude-3
|
||||
automation_profile: trusted
|
||||
definition_of_done: |
|
||||
All classes follow Single Responsibility Principle.
|
||||
No method exceeds 20 lines.
|
||||
invariants:
|
||||
- "No reduction in test coverage"
|
||||
- "All public APIs remain backward compatible"
|
||||
arguments:
|
||||
- name: module_path
|
||||
type: string
|
||||
required: true
|
||||
```
|
||||
|
||||
### `agents action list` / `show` / `archive`
|
||||
|
||||
```bash
|
||||
agents action list # all actions
|
||||
agents action list --namespace local # filter by namespace
|
||||
agents action list --state available # filter by state
|
||||
agents action show local/code-coverage # show action details
|
||||
agents action archive local/old-action # soft-delete action
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Resource Management (`agents resource`)
|
||||
|
||||
Resources are external entities (git repos, directories, mounts) that plans
|
||||
can read or modify.
|
||||
|
||||
### `agents resource add`
|
||||
|
||||
```bash
|
||||
agents resource add git-checkout local/my-repo --path /home/user/projects/my-repo
|
||||
agents resource add fs-directory local/data --path /data --read-only
|
||||
```
|
||||
|
||||
**Built-in resource types (v3.0.0):** `git-checkout`, `fs-directory`, `fs-mount`
|
||||
|
||||
### `agents resource list` / `show` / `remove`
|
||||
|
||||
```bash
|
||||
agents resource list # all resources
|
||||
agents resource show local/my-repo # resource details
|
||||
agents resource remove --yes local/my-repo # remove resource
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Project Management (`agents project`)
|
||||
|
||||
Projects group resources together for plan execution context.
|
||||
|
||||
### `agents project create` / `link-resource` / `list` / `show`
|
||||
|
||||
```bash
|
||||
agents project create local/my-project --description "My application"
|
||||
agents project link-resource local/my-project local/my-repo
|
||||
agents project list
|
||||
agents project show local/my-project
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Plan Lifecycle (`agents plan`)
|
||||
|
||||
Plans are the central execution unit. Lifecycle: **Strategize -> Execute -> Apply**.
|
||||
|
||||
### `agents plan use` — Create a plan
|
||||
|
||||
```bash
|
||||
agents plan use local/code-coverage my-project --arg target_coverage=80
|
||||
agents plan use local/lint proj-1 proj-2 # multiple projects
|
||||
agents plan use local/refactor my-project \ # with automation profile & invariants
|
||||
--automation-profile trusted --invariant "No new warnings"
|
||||
agents plan use local/code-coverage my-project \ # with actor overrides
|
||||
--strategy-actor openai/gpt-4 --execution-actor anthropic/claude-3
|
||||
```
|
||||
|
||||
### `agents plan execute` / `diff` / `apply` — Run and merge changes
|
||||
|
||||
```bash
|
||||
agents plan execute # Strategize -> Execute phases
|
||||
agents plan diff 01HXYZ... # preview proposed changes
|
||||
agents plan apply --yes # merge changes (skip confirmation)
|
||||
```
|
||||
|
||||
### `agents plan list` / `status` / `cancel` — Manage plans
|
||||
|
||||
```bash
|
||||
agents plan list --state complete # filter plans
|
||||
agents plan status 01HXYZ... # plan details
|
||||
agents plan cancel 01HXYZ... --reason "Requirements changed"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## See Also
|
||||
|
||||
- [CLI overview (v3.0.0)](index.md) — Goals, architecture notes, feature table
|
||||
- [Deep Dive](deep-dive.md) — Sandbox, persistence, domain model internals
|
||||
@@ -0,0 +1,95 @@
|
||||
# v3.0.0 — Deep Dive: Sandbox, Persistence & Domain Model
|
||||
|
||||
Internal architecture details for CleverAgents v3.0.0.
|
||||
|
||||
---
|
||||
|
||||
## Git Worktree Sandbox
|
||||
|
||||
The git worktree sandbox isolates LLM-generated changes in a dedicated branch
|
||||
and worktree until the user approves them via `agents plan apply`.
|
||||
|
||||
See the full [Git Worktree Sandbox](../../modules/git-worktree-sandbox.md) module docs.
|
||||
|
||||
### How It Works
|
||||
|
||||
1. Plan execute creates branch `cleveragents/plan-<id>` from current HEAD.
|
||||
2. A temporary git worktree is created in a system temp directory.
|
||||
3. The LLM writes changes only inside the worktree (not the real repo).
|
||||
4. `plan diff` shows proposed changes as unified diff.
|
||||
5. `plan apply` merges sandbox into original via `git merge`, then cleans up.
|
||||
6. `plan cancel` discards the worktree — no real repo changes.
|
||||
|
||||
Non-git resources (e.g. `fs-directory`) fall back to `shutil.copy2`.
|
||||
|
||||
### Apply Summary Output
|
||||
|
||||
```
|
||||
╭─ Apply Summary ────────────────────────────────────────╮
|
||||
│ Plan ID: plan-01HZ... │
|
||||
│ Artifacts: 3 files changed │
|
||||
│ Insertions: +142 │
|
||||
│ Deletions: -17 │
|
||||
╰───────────────────────────────────────────────────────╯
|
||||
╭─ Sandbox Cleanup ──────────────────────────────────────╮
|
||||
│ [x] Worktree removed │
|
||||
│ [x] Branch deleted │
|
||||
╰───────────────────────────────────────────────────────╯
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## SQLite Persistence with Alembic
|
||||
|
||||
All entities persist in `~/.cleveragents/cleveragents.db`.
|
||||
|
||||
### Managed Tables
|
||||
|
||||
| Table | Description |
|
||||
|-------|-------------|
|
||||
| `actions` | Action templates |
|
||||
| `action_arguments`, `action_invariants` | Per-action metadata |
|
||||
| `resources` | Resource instances |
|
||||
| `projects`, `project_resource_links` | Projects and links |
|
||||
| `plans`, `plan_project_links` | Plans and project bindings |
|
||||
| `decisions` | Decision tree nodes per plan |
|
||||
|
||||
### Migration Commands
|
||||
|
||||
```bash
|
||||
alembic upgrade head # apply pending migrations
|
||||
alembic current # show current revision
|
||||
alembic history # migration history
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Domain Model (Pydantic v2)
|
||||
|
||||
All domain entities use Pydantic v2 with `frozen=True` for immutability.
|
||||
|
||||
```python
|
||||
from cleveragents.domain.models import Action, Resource, Project, Plan
|
||||
|
||||
action = Action(
|
||||
name="local/code-coverage",
|
||||
description="Increase code coverage",
|
||||
strategy_actor="openai/gpt-4",
|
||||
execution_actor="openai/gpt-4",
|
||||
definition_of_done="Coverage reaches target.",
|
||||
)
|
||||
# action.name = "other" # raises ValidationError (frozen)
|
||||
```
|
||||
|
||||
ULIDs (Universally Unique Lexicographically Sortable Identifiers) are the primary keys.
|
||||
|
||||
See [ADR-019](../../adr/ADR-019-storage-and-persistence.md) for storage details.
|
||||
|
||||
---
|
||||
|
||||
## See Also
|
||||
|
||||
- [CLI Reference](cli-reference.md) — All command documentation
|
||||
- [Git Worktree Sandbox](../../modules/git-worktree-sandbox.md)
|
||||
- [ADR-006](../../adr/ADR-006-plan-lifecycle.md): Plan Lifecycle
|
||||
- [ADR-019](../../adr/ADR-019-storage-and-persistence.md): Storage & Persistence
|
||||
@@ -0,0 +1,87 @@
|
||||
# v3.0.0 — Minimal Local Source-Code Workflow (M1) — Overview
|
||||
|
||||
**Released:** v3.0.0
|
||||
**Milestone:** M1 — Minimal Local Source-Code Workflow
|
||||
**Theme:** Establish the foundational CLI, domain model, and persistence layer for local agent-driven code workflows.
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
v3.0.0 delivers the first complete end-to-end workflow for using CleverAgents to
|
||||
manage source-code changes on a local machine. It introduces the core CLI command
|
||||
groups (`agents action`, `agents resource`, `agents project`, `agents plan`),
|
||||
a SQLite-backed persistence layer with Alembic migrations, and a git worktree
|
||||
sandbox that isolates LLM-generated changes until the user approves them.
|
||||
|
||||
This milestone establishes the **specification-first** development model: every
|
||||
feature is driven by a YAML-defined action template, executed through a structured
|
||||
plan lifecycle (Strategize -> Execute -> Apply), and persisted in a local SQLite
|
||||
database.
|
||||
|
||||
---
|
||||
|
||||
## Goals
|
||||
|
||||
1. Provide a minimal but complete CLI for local source-code automation.
|
||||
2. Introduce a typed, versioned domain model using Pydantic v2.
|
||||
3. Persist all entities (actions, resources, projects, plans) in SQLite via Alembic.
|
||||
4. Isolate LLM-generated changes in a git worktree sandbox before applying them.
|
||||
5. Enable plan lifecycle management: create, execute, diff, and apply plans.
|
||||
|
||||
---
|
||||
|
||||
## Delivered Features
|
||||
|
||||
v3.0.0 delivers the following feature groups:
|
||||
|
||||
| Feature Group | CLI Commands | Description | Page |
|
||||
|---------------|-------------|-------------|------|
|
||||
| **Action Mgmt** | `agents action` | YAML plan templates | [CLI Reference](cli-reference.md) |
|
||||
| **Resource Mgmt** | `agents resource` | Git repos, filesystems | [CLI Reference](cli-reference.md) |
|
||||
| **Project Mgmt** | `agents project` | Group resources for plans | [CLI Reference](cli-reference.md) |
|
||||
| **Plan Lifecycle** | `agents plan` | Strategy-Execute-Apply cycle | [CLI Reference](cli-reference.md) |
|
||||
| **Git Worktree Sandbox** | (internal) | Isolated LLM change sandbox | [Deep Dive](deep-dive.md) |
|
||||
| **SQLite Persistence** | (internal) | SQLite + Alembic with 9 tables | [Deep Dive](deep-dive.md) |
|
||||
| **Pydantic v2 Domain Model** | (internal) | Frozen=True, ULID keys | [Deep Dive](deep-dive.md) |
|
||||
|
||||
---
|
||||
|
||||
## Architecture Notes
|
||||
|
||||
### Layered Architecture
|
||||
|
||||
v3.0.0 follows a strict layered architecture (see [ADR-001](../../adr/ADR-001-layered-architecture.md)):
|
||||
|
||||
```
|
||||
CLI Layer agents action/resource/project/plan
|
||||
|
|
||||
Application Layer PlanLifecycleService, ActionService, ResourceService
|
||||
|
|
||||
Domain Layer Action, Resource, Project, Plan (Pydantic v2, frozen=True)
|
||||
|
|
||||
Infrastructure SQLite + Alembic, GitWorktreeSandbox, ToolRegistry
|
||||
```
|
||||
|
||||
### Dependency Injection
|
||||
|
||||
All services are wired via a DI container (see [ADR-003](../../adr/ADR-003-dependency-injection.md)).
|
||||
|
||||
### Plan Lifecycle State Machine
|
||||
|
||||
Plans follow a strict state machine (see [ADR-006](../../adr/ADR-006-plan-lifecycle.md)):
|
||||
|
||||
```
|
||||
CREATED -> STRATEGIZE/queued -> STRATEGIZE/complete
|
||||
-> EXECUTE/queued -> EXECUTE/complete
|
||||
-> APPLY/queued -> APPLY/complete -> CANCELLED / ERRORED (terminal)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## See Also
|
||||
|
||||
- [CLI Reference](cli-reference.md) — Full command-by-command documentation
|
||||
- [Deep Dive](deep-dive.md) — Sandbox, persistence, domain model internals
|
||||
- [ADR-001](../../adr/ADR-001-layered-architecture.md): Layered Architecture
|
||||
- [ADR-006](../../adr/ADR-006-plan-lifecycle.md): Plan Lifecycle
|
||||
@@ -0,0 +1,131 @@
|
||||
# v3.1.0 — Actor YAML Format & Compiler
|
||||
|
||||
Declarative actor definitions compiled into LangGraph graphs.
|
||||
|
||||
---
|
||||
|
||||
## Actor YAML Format
|
||||
|
||||
Every actor file must specify `version: "3"` and one of three types: `llm`, `tool`, or `graph`.
|
||||
|
||||
See [Actor YAML Schema Reference](../../reference/actors_schema.md) for full fields.
|
||||
|
||||
### LLM Actor
|
||||
|
||||
```yaml
|
||||
version: "3"
|
||||
name: assistants/code-reviewer
|
||||
type: llm
|
||||
model: gpt-4
|
||||
system_prompt: |
|
||||
You are an expert Python code reviewer.
|
||||
context_view: reviewer
|
||||
memory:
|
||||
enabled: true
|
||||
max_messages: 20
|
||||
```
|
||||
|
||||
### Tool Actor
|
||||
|
||||
```yaml
|
||||
version: "3"
|
||||
name: utilities/file-ops
|
||||
type: tool
|
||||
tools:
|
||||
- files/read_file
|
||||
- files/write_file
|
||||
- files/list_directory
|
||||
```
|
||||
|
||||
### Graph Actor
|
||||
|
||||
```yaml
|
||||
version: "3"
|
||||
name: workflows/tdd-cycle
|
||||
type: graph
|
||||
model: gpt-4
|
||||
route:
|
||||
nodes:
|
||||
- id: planner; type: agent; name: Task Planner
|
||||
config: { model: gpt-4, prompt: "Plan implementation tasks." }
|
||||
- id: implementer; type: agent; name: Code Implementer
|
||||
config: { model: gpt-4, prompt: "Implement planned tasks." }
|
||||
- id: verifier; type: tool; name: Test Runner
|
||||
config: { tool_name: testing/run_pytest }
|
||||
- id: check_results; type: conditional
|
||||
config: { conditions: [{check: "passed==True", route_to: done}, {check: "passed==False", route_to: implementer}] }
|
||||
- id: done; type: agent; name: Summary Writer
|
||||
edges:
|
||||
- {from_node: planner, to_node: implementer}
|
||||
- {from_node: implementer, to_node: verifier}
|
||||
- {from_node: verifier, to_node: check_results}
|
||||
entry_node: planner
|
||||
exit_nodes: [done]
|
||||
```
|
||||
|
||||
### Actor Name Format
|
||||
|
||||
Must use `namespace/name`:
|
||||
- `assistants/code-reviewer` — valid
|
||||
- `code-reviewer` — invalid (no namespace)
|
||||
- `a/b/c` — invalid (too many slashes)
|
||||
|
||||
---
|
||||
|
||||
## Actor Compiler (LangGraph Integration)
|
||||
|
||||
Translates GRAPH-type YAML into LangGraph `StateGraph` structures.
|
||||
|
||||
See [Actor Compiler Reference](../../reference/actor_compiler.md).
|
||||
|
||||
### Compilation Pipeline
|
||||
|
||||
```
|
||||
ActorConfigSchema (type=GRAPH)
|
||||
-> validate type, route
|
||||
-> reference validation (all node IDs exist)
|
||||
-> intra-graph cycle detection
|
||||
-> cross-actor subgraph cycle detection
|
||||
-> node mapping (NodeDef -> LangGraph NodeConfig)
|
||||
-> edge mapping
|
||||
-> metadata assembly
|
||||
=> CompiledActor (nodes, edges, entry_point, metadata)
|
||||
```
|
||||
|
||||
### Node Type Mapping
|
||||
|
||||
| Actor Node | LangGraph Type |
|
||||
|------------|----------------|
|
||||
| `agent` | AGENT |
|
||||
| `tool` | TOOL |
|
||||
| `conditional` | CONDITIONAL |
|
||||
| `subgraph` | SUBGRAPH |
|
||||
|
||||
### Compilation Errors
|
||||
|
||||
| Error | Class | When |
|
||||
|-------|-------|------|
|
||||
| Non-GRAPH type | `ActorCompilationError` | config.type != GRAPH |
|
||||
| Missing route | `ActorCompilationError` | config.route is None |
|
||||
| Missing node | `MissingNodeError` | edge refs unknown node |
|
||||
| Invalid entry/exit | `InvalidEntryExitError` | node not in graph |
|
||||
| Cycle detected | `SubgraphCycleError` | intra or cross-actor cycle |
|
||||
|
||||
### Graph Validation Rules
|
||||
|
||||
1. Unique node IDs within graph
|
||||
2. Entry node exists and is valid
|
||||
3. All exit_nodes reference valid nodes
|
||||
4. All edge from_node/to_node exist
|
||||
5. No cycles (graphs are acyclic)
|
||||
6. All nodes reachable from entry_node
|
||||
|
||||
---
|
||||
|
||||
## See Also
|
||||
|
||||
- [Actor YAML Schema Reference](../../reference/actors_schema.md)
|
||||
- [Actor Compiler Reference](../../reference/actor_compiler.md)
|
||||
- [ADR-010](../../adr/ADR-010-actor-and-agent-architecture.md): Actor Architecture
|
||||
- [ADR-022](../../adr/ADR-022-langchain-langgraph-integration.md): LangGraph Integration
|
||||
- [ADR-031](../../adr/ADR-031-actor-abstraction-definition.md): Actor Abstraction Definition
|
||||
@@ -0,0 +1,69 @@
|
||||
# v3.1.0 — Actor Compiler + Full LLM Integration (M2) — Overview
|
||||
|
||||
**Released:** v3.1.0
|
||||
**Milestone:** M2 — Actor Compiler + Full LLM Integration
|
||||
**Theme:** Introduce the Actor system, compile YAML-defined actors into executable LangGraph graphs, and wire full LLM integration through MCP adapter, tool router, skill registry, and validation runner.
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
v3.1.0 builds on the v3.0.0 foundation by introducing the **Actor system** —
|
||||
declarative YAML definitions for AI agents compiled into LangGraph `StateGraph`
|
||||
structures at runtime. It also delivers MCP adapter (external tool server
|
||||
connectivity), tool router (provider normalization), skill registry (reusable
|
||||
tool collections), and validation runner (pre-apply quality gates).
|
||||
|
||||
---
|
||||
|
||||
## Goals
|
||||
|
||||
1. Define a declarative YAML format for actors (`llm`, `tool`, `graph`).
|
||||
2. Compile GRAPH-type actors into executable LangGraph `StateGraph` structures.
|
||||
3. Connect to external MCP tool servers for tool discovery and invocation.
|
||||
4. Route LLM tool calls across OpenAI, Anthropic, and LangChain formats.
|
||||
5. Manage reusable tool collections via the skill registry.
|
||||
6. Enforce validation gates before plan apply via the validation runner.
|
||||
|
||||
---
|
||||
|
||||
## Feature Index
|
||||
|
||||
| Feature | Description | Documentation |
|
||||
|---------|-------------|---------------|
|
||||
| **Actor YAML** | Declarative defs: llm, tool, graph types | [Actor Format](actor-yaml.md) |
|
||||
| **Actor Compiler** | Compile to LangGraph StateGraph | [Actor Format](actor-yaml.md) |
|
||||
| **MCP Adapter** | External MCP tool server connectivity | [Integration](integration.md) |
|
||||
| **Tool Router** | Normalize across OpenAI/Anthropic/LC formats | [Integration](integration.md) |
|
||||
| **Skill Registry** | Persistent, composable tool collections | [Skills](skills.md) |
|
||||
| **Validation Runner** | Pre-apply required/informational gates | [Skills](skills.md) |
|
||||
|
||||
---
|
||||
|
||||
## Architecture Notes
|
||||
|
||||
### Actor System Design
|
||||
|
||||
Follows the **Actor Abstraction Definition** (see [ADR-031](../../adr/ADR-031-actor-abstraction-definition.md)):
|
||||
- **Declarative** — defined in YAML, not code.
|
||||
- **Composable** — GRAPH actors embed other actors as subgraphs.
|
||||
- **Compiled** — validated and compiled to LangGraph at load time.
|
||||
- **Namespaced** — `namespace/name` format required.
|
||||
|
||||
### LangGraph Integration
|
||||
|
||||
See [ADR-022](../../adr/ADR-022-langchain-langgraph-integration.md). The compiler
|
||||
takes GRAPH actors through an 8-step pipeline, producing LangGraph `StateGraph` structures.
|
||||
|
||||
### MCP & Skills
|
||||
|
||||
MCP adapter implements Model Context Protocol (see [ADR-029](../../adr/ADR-029-model-context-protocol.md)). Skill registry implements the Skill Abstraction Definition (see [ADR-030](../../adr/ADR-030-skill-abstraction-definition.md) and [ADR-012](../../adr/ADR-012-skill-system.md)).
|
||||
|
||||
---
|
||||
|
||||
## See Also
|
||||
|
||||
- [Actor Format & Compiler](actor-yaml.md)
|
||||
- [MCP + Tool Router](integration.md)
|
||||
- [Skills + Validation Runner](skills.md)
|
||||
- [Quick Start Guide](quickstart.md) — common workflows
|
||||
@@ -0,0 +1,91 @@
|
||||
# v3.1.0 — MCP Adapter & Tool Router
|
||||
|
||||
External tool server integration and LLM provider normalization.
|
||||
|
||||
---
|
||||
|
||||
## MCP Adapter
|
||||
|
||||
The Model Context Protocol adapter connects CleverAgents to external tool servers.
|
||||
|
||||
### Supported Transports
|
||||
|
||||
| Transport | Description | Config Required |
|
||||
|-----------|-------------|-----------------|
|
||||
| `stdio` | Spawns subprocess | `command` |
|
||||
| `sse` | Server-Sent Events HTTP | `url` |
|
||||
| `streamable-http` | Streamable HTTP | `url` |
|
||||
|
||||
### Configuration & Usage
|
||||
|
||||
```python
|
||||
from cleveragents.mcp.adapter import MCPToolAdapter, MCPServerConfig
|
||||
|
||||
config = MCPServerConfig(
|
||||
name="my-tools", transport="stdio", command="python",
|
||||
args=["-m", "my_mcp_server"], env={"API_KEY": "secret"},
|
||||
)
|
||||
adapter = MCPToolAdapter(config)
|
||||
adapter.connect(timeout=10.0)
|
||||
tools = adapter.discover_tools()
|
||||
result = adapter.invoke("create_issue", {"title": "Bug"})
|
||||
# result: {success: true, data: {"issue_id": 42}, duration_ms: 145.3}
|
||||
adapter.disconnect()
|
||||
```
|
||||
|
||||
### ToolRegistry Integration
|
||||
|
||||
```python
|
||||
from cleveragents.tool.registry import ToolRegistry
|
||||
registry = ToolRegistry()
|
||||
adapter.register_tools(registry, namespace="my-tools")
|
||||
# Tools available as "my-tools/<tool_name>"
|
||||
```
|
||||
|
||||
### Capability Inference (name-based heuristics)
|
||||
|
||||
- `read`, `get`, `list`, `search`, `find` — inferred as read-only
|
||||
- `write`, `create`, `update`, `delete`, `set` — inferred as writable
|
||||
|
||||
### Reconnect on Failure
|
||||
|
||||
```python
|
||||
try: adapter.invoke("tool", {"arg": "x"})
|
||||
except RuntimeError:
|
||||
adapter.reconnect(timeout=10.0)
|
||||
adapter.discover_tools()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Tool Router
|
||||
|
||||
Normalizes LLM provider tool calls into a unified internal representation.
|
||||
|
||||
### Supported Provider Formats
|
||||
|
||||
| Provider | Shape | Arguments Key | Type |
|
||||
|----------|-------|---------------|------|
|
||||
| OpenAI | `{"name": "...", "arguments": "..."}` | `arguments` | JSON string |
|
||||
| Anthropic | `{"name": "...", "input": {...}}` | `input` | dict |
|
||||
| LangChain | `{"name": "...", "type": "tool_call", "args": {...}}` | `args` | dict |
|
||||
|
||||
### Usage
|
||||
|
||||
```python
|
||||
from cleveragents.tool.router import ToolCallRouter
|
||||
router = ToolCallRouter(registry, runner, plan_id="plan-001")
|
||||
result = router.route({"name": "files/read_file", 'arguments': '{"path": "a.py"}'})
|
||||
# result.provider_format: "openai" result.tool_call_id: "tc_<24-hex>"
|
||||
|
||||
# Stable IDs: generate_tool_call_id("plan-001", 0) always returns same value
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## See Also
|
||||
|
||||
- [MCP Tool Adapter Reference](../../reference/mcp_adapter.md)
|
||||
- [Tool Call Router Reference](../../reference/tool_router.md)
|
||||
- [ADR-011](../../adr/ADR-011-tool-system.md): Tool System
|
||||
- [ADR-029](../../adr/ADR-029-model-context-protocol.md): MCP Adoption
|
||||
@@ -0,0 +1,66 @@
|
||||
# v3.1.0 — Quick Start Guide
|
||||
|
||||
Common workflows: actor definitions, MCP setup, skills, & validation gates.
|
||||
|
||||
---
|
||||
|
||||
## Step-by-Step Workflow
|
||||
|
||||
```bash
|
||||
# 1. Define a graph actor YAML
|
||||
cat > actors/tdd.yaml << 'EOF'
|
||||
version: "3"
|
||||
name: assistants/tdd
|
||||
type: graph
|
||||
model: gpt-4
|
||||
route:
|
||||
nodes:
|
||||
- id: planner; type: agent; name: Planner
|
||||
config: { model: gpt-4, prompt: "Plan tasks from requirements." }
|
||||
- id: implementer; type: agent; name: Implementer
|
||||
config: { model: gpt-4, prompt: "Write clean tested code." }
|
||||
edges: [{from_node: planner, to_node: implementer}]
|
||||
entry_node: planner
|
||||
exit_nodes: [implementer]
|
||||
EOF
|
||||
|
||||
# 2. Create an action using the actor
|
||||
cat > review-action.yaml << 'EOF'
|
||||
name: local/code-review
|
||||
description: Review code changes
|
||||
execution_actor: assistants/tdd
|
||||
definition_of_done: |
|
||||
All changes reviewed for correctness and style.
|
||||
EOF
|
||||
agents action create --config review-action.yaml
|
||||
|
||||
# 3. Configure MCP tool server in ~/.cleveragents/config.toml
|
||||
|
||||
# 4. Register a skill
|
||||
agents skill add --config my-skill.yaml
|
||||
|
||||
# 5. Execute plan with validation gate
|
||||
agents plan use local/code-review my-project
|
||||
agents plan execute # runs validation before apply
|
||||
agents plan diff
|
||||
agents plan apply --yes # blocked if required validations fail
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Actor Creation Checklist
|
||||
|
||||
- [ ] `version: "3"` specified
|
||||
- [ ] Valid type: `llm`, `tool`, or `graph`
|
||||
- [ ] Name follows `namespace/name` format
|
||||
- [ ] All referenced actors and tools are resolvable
|
||||
- [ ] Graph actors have valid entry/exit nodes and acyclic edges
|
||||
|
||||
---
|
||||
|
||||
## Skill & Validation Quick Tips
|
||||
|
||||
- Skills can include other skills for composition
|
||||
- Agent Skills Standard discovers skills from filesystem paths in `agent_skills_paths`
|
||||
- Validations run deterministically: alphabetical (resource, mode, name) order
|
||||
- Run with `max_workers=1` in CI for predictable sequential validation
|
||||
@@ -0,0 +1,127 @@
|
||||
# v3.1.0 — Skill Registry & Validation Runner
|
||||
|
||||
Reusable tool collections and pre-apply validation quality gates.
|
||||
|
||||
---
|
||||
|
||||
## Skill Registry
|
||||
|
||||
Persistent storage for named, reusable tool collections in SQLite.
|
||||
|
||||
### Item Types
|
||||
|
||||
| Type | Description |
|
||||
|------|-------------|
|
||||
| `tool_ref` | Reference to a named tool |
|
||||
| `include` | Recursive inclusion of another skill |
|
||||
| `inline_tool` | Anonymous tool defined inline |
|
||||
| `mcp_source` | MCP server tool source |
|
||||
| `agent_source` | Agent Skills Standard folder source |
|
||||
|
||||
### CLI Commands
|
||||
|
||||
```bash
|
||||
agents skill list # all skills
|
||||
agents skill tools local/devops-toolkit # show skill's tools
|
||||
agents skill add --config ./my-skill.yaml # register from YAML
|
||||
agents skill remove local/my-skill # remove a skill
|
||||
```
|
||||
|
||||
### Service API
|
||||
|
||||
```python
|
||||
from cleveragents.application.services import SkillRegistryService
|
||||
svc = SkillRegistryService(skill_repo=SkillRepository(session_factory))
|
||||
svc.add_skill(skill) # register
|
||||
skill = svc.get_skill("local/code-tools") # retrieve
|
||||
skills = svc.list_skills(namespace="local") # filter
|
||||
svc.remove_skill("local/code-tools") # delete
|
||||
```
|
||||
|
||||
### Agent Skills Standard Integration
|
||||
|
||||
Filesystem-based tool bundles with `SKILL.md` + YAML front-matter.
|
||||
|
||||
**Default discovery path:** `~/.cleveragents/agent_skills`
|
||||
|
||||
**SKILL.md format:**
|
||||
|
||||
```markdown
|
||||
---
|
||||
name: my-tool
|
||||
description: A custom agent skill
|
||||
input_schema: {type: object, properties: {query: {type: string}}}
|
||||
---
|
||||
|
||||
# My Tool
|
||||
Additional documentation.
|
||||
```
|
||||
|
||||
### Configuration
|
||||
|
||||
```toml
|
||||
[skills]
|
||||
agent_skills_paths = "/home/user/.cleveragents/agent_skills,/opt/skills"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Validation Runner
|
||||
|
||||
Executes validations against plan resources before Apply phase.
|
||||
|
||||
### Modes
|
||||
|
||||
| Mode | Pass Behavior | Fail Behavior |
|
||||
|------|---------------|----------------|
|
||||
| `required` | OK | **Blocks apply** — sets `all_required_passed = False` |
|
||||
| `informational` | Logged | Logged only — does NOT block apply |
|
||||
|
||||
### Attachment Scopes
|
||||
|
||||
| Scope | Active When |
|
||||
|-------|-------------|
|
||||
| direct | Always for the resource |
|
||||
| project | Resource accessed via a project |
|
||||
| plan | Only for specific plan |
|
||||
|
||||
### Apply Validation Gate
|
||||
|
||||
```python
|
||||
from cleveragents.validation.gate import ApplyValidationGate
|
||||
gate = ApplyValidationGate(runner=DefaultValidationRunner())
|
||||
summary = gate.run(plan_id, attachments, context)
|
||||
if gate.should_block_apply(summary):
|
||||
# report failure reasons, block apply
|
||||
else:
|
||||
# proceed
|
||||
```
|
||||
|
||||
### Blocking vs Allowed Output
|
||||
|
||||
**Blocked:**
|
||||
```
|
||||
Validation Gate: BLOCKED
|
||||
Required: 1 passed, 1 failed
|
||||
Failures: lint-check on res-001: 3 lint errors found
|
||||
```
|
||||
|
||||
**Allowed:**
|
||||
```
|
||||
Validation Gate: PASSED
|
||||
Required: 2 passed, 0 failed
|
||||
Informational: 1 passed, 0 failed
|
||||
```
|
||||
|
||||
### Concurrency
|
||||
|
||||
Uses `ThreadPoolExecutor` with configurable `max_workers` (default 4).
|
||||
|
||||
---
|
||||
|
||||
## See Also
|
||||
|
||||
- [Skill Registry Reference](../../reference/skill_registry.md)
|
||||
- [Validation Pipeline Reference](../../reference/validation_pipeline.md)
|
||||
- [ADR-012](../../adr/ADR-012-skill-system.md): Skill System
|
||||
- [ADR-013](../../adr/ADR-013-validation-abstraction.md): Validation Abstraction
|
||||
+67
-54
@@ -88,8 +88,8 @@ via the `getExtendedAgentCard` operation.
|
||||
|
||||
## Transport Modes
|
||||
|
||||
| Mode | Transport | Class / SDK Component | Behaviour |
|
||||
|--------|------------------------|------------------------------------|--------------------------------------------------------------------|
|
||||
| Mode | Transport | Class / SDK Component | Behaviour |
|
||||
|--------|------------------------|------------------------------------|--------------------------------------------------------|
|
||||
| Local | A2A JSON-RPC over stdio | `A2aLocalFacade` + JSON-RPC stdio | Agent as subprocess; extensions resolved in-process |
|
||||
| Server | A2A JSON-RPC over HTTP | A2A SDK HTTP transport | JSON-RPC 2.0 to CleverAgents server |
|
||||
|
||||
@@ -165,7 +165,7 @@ Pattern applies to all entity types (`actor`, `skill`, `tool`, `validation`,
|
||||
### Context Operations
|
||||
|
||||
| Method | Service Method | Required Params |
|
||||
|--------|----------------|-----------------|
|
||||
|--------|---------------|-----------------|
|
||||
| `_cleveragents/context/show` | `ContextService.show()` | `project_name` |
|
||||
| `_cleveragents/context/inspect` | `ContextService.inspect()` | `project_name` |
|
||||
| `_cleveragents/context/simulate` | `ContextService.simulate()` | `project_name`, simulation params |
|
||||
@@ -174,7 +174,7 @@ Pattern applies to all entity types (`actor`, `skill`, `tool`, `validation`,
|
||||
### Sync Operations
|
||||
|
||||
| Method | Service Method | Required Params |
|
||||
|--------|----------------|-----------------|
|
||||
|--------|---------------|-----------------|
|
||||
| `_cleveragents/sync/pull` | `SyncService.pull()` | `namespace` |
|
||||
| `_cleveragents/sync/push` | `SyncService.push()` | `namespace`, `entities` |
|
||||
| `_cleveragents/sync/status` | `SyncService.status()` | `namespace` (optional) |
|
||||
@@ -182,7 +182,7 @@ Pattern applies to all entity types (`actor`, `skill`, `tool`, `validation`,
|
||||
### Namespace Operations
|
||||
|
||||
| Method | Service Method | Required Params |
|
||||
|--------|----------------|-----------------|
|
||||
|--------|---------------|-----------------|
|
||||
| `_cleveragents/namespace/list` | `NamespaceService.list()` | -- |
|
||||
| `_cleveragents/namespace/show` | `NamespaceService.show()` | `namespace` |
|
||||
| `_cleveragents/namespace/members` | `NamespaceService.members()` | `namespace` |
|
||||
@@ -198,20 +198,13 @@ Pattern applies to all entity types (`actor`, `skill`, `tool`, `validation`,
|
||||
|
||||
## Streaming Events
|
||||
|
||||
A2A streaming uses SSE via the `message/stream` operation (client → server
|
||||
request), delivering typed events as the task progresses. The SSE data
|
||||
payloads are JSON-RPC 2.0 **notifications** (no `id` field):
|
||||
A2A streaming uses SSE via the `message/stream` operation, delivering typed
|
||||
events as the task progresses:
|
||||
|
||||
| Event Type | JSON-RPC Method | Emitted When |
|
||||
|------------|-----------------|--------------|
|
||||
| `TaskStatusUpdateEvent` | `task/statusUpdate` | Task state changes (submitted -> working -> completed, etc.) |
|
||||
| `TaskArtifactUpdateEvent` | `task/artifactUpdate` | Agent produces output (response chunks, plan entries, tool results) |
|
||||
|
||||
> **Note:** `message/stream` is the **request** method (client → server) that
|
||||
> initiates streaming. The SSE data payloads use `task/statusUpdate` or
|
||||
> `task/artifactUpdate` as the notification method name — not `message/stream`.
|
||||
> Non-spec fields (`event_id`, `event_type`, `timestamp`) are carried in SSE
|
||||
> envelope headers (`event:` and `id:` lines), not in the data payload.
|
||||
| Event Type | Emitted When |
|
||||
|-----------|-------------|
|
||||
| `TaskStatusUpdateEvent` | Task state changes (submitted -> working -> completed, etc.) |
|
||||
| `TaskArtifactUpdateEvent` | Agent produces output (response chunks, plan entries, tool results) |
|
||||
|
||||
### Task Lifecycle States
|
||||
|
||||
@@ -227,30 +220,16 @@ payloads are JSON-RPC 2.0 **notifications** (no `id` field):
|
||||
|
||||
### Example Streaming Event
|
||||
|
||||
SSE data payload for a task status update (JSON-RPC 2.0 notification):
|
||||
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "task/statusUpdate",
|
||||
"method": "message/stream",
|
||||
"params": {
|
||||
"taskId": "task_01HXR...",
|
||||
"status": { "state": "working" }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
SSE data payload for an artifact update:
|
||||
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "task/artifactUpdate",
|
||||
"params": {
|
||||
"taskId": "task_01HXR...",
|
||||
"artifact": {
|
||||
"id": "task_01HXR...",
|
||||
"status": { "state": "working" },
|
||||
"artifacts": [{
|
||||
"parts": [{ "text": "I'll start by extracting..." }]
|
||||
}
|
||||
}]
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -298,9 +277,9 @@ response = facade.dispatch(A2aRequest(
|
||||
|
||||
### Constructor
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|------------|----------------------------------|---------|--------------------------------|
|
||||
| `services` | `dict[str, Any] \| None` | `None` | Named services for routing |
|
||||
| Parameter | Type | Default | Description |
|
||||
|------------|----------------------------|---------|-------------------------------|
|
||||
| `services` | `dict[str, Any] \| None` | `None` | Named services for routing |
|
||||
|
||||
### Methods
|
||||
|
||||
@@ -320,15 +299,15 @@ registered later with `register_service()`.
|
||||
|
||||
### Service Keys
|
||||
|
||||
| Key | Type | Wired Methods |
|
||||
|----------------------------------|----------------------------------|--------------------------------------------|
|
||||
| `session_service` | `SessionWorkflow` | Standard message operations |
|
||||
| `plan_lifecycle_service` | `PlanLifecycleService` | `_cleveragents/plan/*` |
|
||||
| `tool_registry` | `ToolRegistry` | `_cleveragents/registry/tool/*` |
|
||||
| `resource_registry_service` | `ResourceRegistryService` | `_cleveragents/registry/resource/*` |
|
||||
| `sync_service` | `SyncService` | `_cleveragents/sync/*` |
|
||||
| `namespace_service` | `NamespaceService` | `_cleveragents/namespace/*` |
|
||||
| `context_service` | `ContextService` | `_cleveragents/context/*` |
|
||||
| Key | Type | Wired Methods |
|
||||
|------------------------------|-----------------------------|-------------------------------------|
|
||||
| `session_service` | `SessionWorkflow` | Standard message operations |
|
||||
| `plan_lifecycle_service` | `PlanLifecycleService` | `_cleveragents/plan/*` |
|
||||
| `tool_registry` | `ToolRegistry` | `_cleveragents/registry/tool/*` |
|
||||
| `resource_registry_service` | `ResourceRegistryService` | `_cleveragents/registry/resource/*` |
|
||||
| `sync_service` | `SyncService` | `_cleveragents/sync/*` |
|
||||
| `namespace_service` | `NamespaceService` | `_cleveragents/namespace/*` |
|
||||
| `context_service` | `ContextService` | `_cleveragents/context/*` |
|
||||
|
||||
---
|
||||
|
||||
@@ -423,8 +402,42 @@ CleverAgentsError
|
||||
└── A2aOperationNotFoundError
|
||||
```
|
||||
|
||||
| Exception | When Raised |
|
||||
|------------------------------|--------------------------------------------------------------------|
|
||||
| `A2aNotAvailableError` | Server-mode operation attempted without connection |
|
||||
| `A2aVersionMismatchError` | Unsupported A2A version |
|
||||
| `A2aOperationNotFoundError` | Unknown method dispatched |
|
||||
| Exception | When Raised |
|
||||
|-----------------------------|----------------------------------------------------|
|
||||
| `A2aNotAvailableError` | Server-mode operation attempted without connection |
|
||||
| `A2aVersionMismatchError` | Unsupported A2A version |
|
||||
| `A2aOperationNotFoundError` | Unknown method dispatched |
|
||||
|
||||
---
|
||||
|
||||
## ACP to A2A Migration
|
||||
|
||||
> **v3.6.0 breaking change.** The `cleveragents.acp` module was renamed to
|
||||
> `cleveragents.a2a` and all `Acp*` symbols were renamed to `A2a*`. The
|
||||
> request/response wire format was updated to comply with JSON-RPC 2.0.
|
||||
|
||||
For a complete migration guide including symbol rename tables, field name
|
||||
changes, operation name mapping, and YAML configuration updates, see
|
||||
[`docs/development/acp-to-a2a-migration.md`](../development/acp-to-a2a-migration.md).
|
||||
|
||||
### Quick Reference: Deprecated Legacy Operations
|
||||
|
||||
The following ACP operation names are accepted by `A2aLocalFacade.dispatch()`
|
||||
in `cleveragents.a2a.facade.A2aLocalFacade` (commit `449c33b7`) for backward
|
||||
compatibility but are **deprecated**:
|
||||
|
||||
| Deprecated (ACP) | Replacement (A2A) |
|
||||
|---|---|
|
||||
| `session.create` | `_cleveragents/session/create` |
|
||||
| `session.close` | `_cleveragents/session/close` |
|
||||
| `plan.create` | `_cleveragents/plan/use` |
|
||||
| `plan.execute` | `_cleveragents/plan/execute` |
|
||||
| `plan.status` | `_cleveragents/plan/status` |
|
||||
| `plan.diff` | `_cleveragents/plan/diff` |
|
||||
| `plan.apply` | `_cleveragents/plan/apply` |
|
||||
| `registry.list_tools` | `_cleveragents/registry/tool/list` |
|
||||
| `registry.list_resources` | `_cleveragents/registry/resource/list` |
|
||||
| `context.get` | `_cleveragents/context/show` |
|
||||
|
||||
See [ADR-047](../adr/ADR-047-acp-standard-adoption.md) for the architectural
|
||||
rationale behind this change.
|
||||
|
||||
+45
-9
@@ -10,14 +10,14 @@ The following chart shows all 29 epics across 9 legendary workstreams, 7 milesto
|
||||
@startgantt
|
||||
|
||||
title CleverAgents Core — Epic-Level Project Schedule
|
||||
footer Generated 2026-04-15 | 29 Epics | 9 Legendaries | 7 Milestones | 6 Developers | ~1650 SP | ~3 open bugs | 257 open PRs | Session 5 active
|
||||
footer Generated 2026-04-17 | 29 Epics | 9 Legendaries | 7 Milestones | 6 Developers | ~1650 SP | ~3 open bugs | 328 open PRs | Session 4 active
|
||||
|
||||
Project starts 2026-02-03
|
||||
saturday are closed
|
||||
sunday are closed
|
||||
printscale weekly zoom 2
|
||||
|
||||
today is 2026-04-15
|
||||
today is 2026-04-17
|
||||
today is colored in #FF6666
|
||||
|
||||
' ================================================================
|
||||
@@ -49,6 +49,16 @@ today is colored in #FF6666
|
||||
' v3.8.0: 29% (145/509), v3.9.0: 41% (24/59).
|
||||
' Active milestone: v3.2.0 (933 open), v3.5.0 (1,107 open).
|
||||
' ================================================================
|
||||
' ================================================================
|
||||
' GANTT CHART UPDATE LOG (Day 107 - 2026-04-17)
|
||||
' Changes: Day 107 refresh (cycle 1). Session 4 active (issue #4799, 32 workers).
|
||||
' v3.0.0 CLOSED (100%, 0 open/164 closed), v3.1.0 CLOSED (100%, 0 open/108 closed).
|
||||
' M3 (v3.2.0) 25.0% (323/1291, 968 open), M4 (v3.3.0) 34.9% (118/338, 220 open),
|
||||
' M5 (v3.4.0) 35.6% (158/444, 286 open), M6 (v3.5.0) 17.9% (247/1377, 1130 open),
|
||||
' M7 (v3.6.0) 32.8% (162/494, 332 open), M8 (v3.7.0) 41.9% (438/1046, 608 open),
|
||||
' M9 (v3.8.0) 28.2% (144/511, 367 open), v3.9.0 37.5% (24/64, 40 open).
|
||||
' Open PRs: 257->328 (+71). AUTO-BUG-SUP frozen alert (issue #10222).
|
||||
' ================================================================
|
||||
|
||||
<style>
|
||||
ganttDiagram {
|
||||
@@ -249,7 +259,7 @@ ganttDiagram {
|
||||
|
||||
[Decisions + Validations M3 (#357)] as [M3] on {Luis} {Hamza} {Brent} {Jeff} requires 4 days
|
||||
[M3] starts at [M2]'s end
|
||||
[M3] is 28% completed
|
||||
[M3] is 25% completed
|
||||
[M3] is colored in LightSkyBlue/SteelBlue
|
||||
|
||||
' ── v3.3.0 MILESTONE ─────────────────────────────────────────
|
||||
@@ -270,13 +280,13 @@ ganttDiagram {
|
||||
|
||||
[Corrections + Checkpoints M4 (#358)] as [M4] on {Luis} {Jeff} {Brent} {Hamza} requires 3 days
|
||||
[M4] starts at [M3]'s end
|
||||
[M4] is 46% completed
|
||||
[M4] is 35% completed
|
||||
[M4] is colored in LightSkyBlue/SteelBlue
|
||||
|
||||
|
||||
[Security + Safety Hardening (#362)] as [SEC] on {Luis} requires 3 days
|
||||
[SEC] starts 2026-03-02
|
||||
[SEC] is 80% completed
|
||||
[SEC] is 28% completed
|
||||
[SEC] is colored in LightSkyBlue/SteelBlue
|
||||
|
||||
[Provider Fixes + Runtime (#363)] as [PROV] on {Luis} requires 2 days
|
||||
@@ -301,12 +311,12 @@ ganttDiagram {
|
||||
|
||||
[ACMS v1 + Context Scaling M5 (#359)] as [M5] on {Hamza} {Jeff} {Aditya} {Brent} requires 4 days
|
||||
[M5] starts at [M4]'s end
|
||||
[M5] is 40% completed
|
||||
[M5] is 36% completed
|
||||
[M5] is colored in LightSkyBlue/SteelBlue
|
||||
|
||||
[Autonomy Hardening + Stubs M6 (#360)] as [M6] on {Luis} {Jeff} {Hamza} {Brent} requires 5 days
|
||||
[M6] starts at [M5]'s end
|
||||
[M6] is 17% completed
|
||||
[M6] is 18% completed
|
||||
[M6] is colored in LightSkyBlue/SteelBlue
|
||||
|
||||
|
||||
@@ -317,7 +327,7 @@ ganttDiagram {
|
||||
|
||||
[Large Project Autonomy (#369)] as [LARGE] on {Luis} {Brent} {Jeff} requires 4 days
|
||||
[LARGE] starts 2026-03-17
|
||||
[LARGE] is 35% completed
|
||||
[LARGE] is 42% completed
|
||||
[LARGE] is colored in LightSkyBlue/SteelBlue
|
||||
|
||||
' ── v3.5.0 MILESTONE ─────────────────────────────────────────
|
||||
@@ -530,6 +540,32 @@ Deadline risk: CRITICAL
|
||||
| M9 (v3.8.0) | (no deadline) -> ETA 2026-08-15 | N/A | MEDIUM |
|
||||
| v3.9.0 | (no deadline) -> ETA 2026-09-01 | N/A | LOW |
|
||||
|
||||
---
|
||||
|
||||
#### Schedule Adherence - 2026-04-18
|
||||
|
||||
| Date | Day | Cycle | Milestone | Planned % | Actual % | Delta | Risk | Notes |
|
||||
|------|-----|-------|-----------|-----------|----------|-------|------|-------|
|
||||
| 2026-04-18 | D107 | — | v3.2.0 M3 | ~50% | 25.7% | -24.3% | CRITICAL | 959 open issues |
|
||||
| 2026-04-18 | D107 | — | v3.3.0 M4 | ~60% | 35.5% | -24.5% | HIGH | 218 open issues |
|
||||
| 2026-04-18 | D107 | — | v3.4.0 M5 | ~60% | 35.5% | -24.5% | HIGH | 287 open issues |
|
||||
| 2026-04-18 | D107 | — | v3.5.0 M6 | ~50% | 18.1% | -31.9% | CRITICAL | 1,130 open issues |
|
||||
| 2026-04-18 | D107 | — | v3.6.0 M7 | ~60% | 32.8% | -27.2% | HIGH | 332 open issues |
|
||||
| 2026-04-18 | D107 | — | v3.7.0 M8 | — | 41.8% | — | HIGH | 608 open, ETA ~2026-07-04 |
|
||||
| 2026-04-18 | D107 | — | v3.8.0 M9 | — | 28.2% | — | MEDIUM | 367 open, ETA ~2026-09-06 |
|
||||
| 2026-04-18 | D107 | — | v3.9.0 | — | 37.5% | — | LOW | 40 open, ETA ~2026-05-10 |
|
||||
|
||||
## Daily Snapshot - 2026-04-18
|
||||
|
||||
| Milestone | Completed % | Open Issues | Risk Assessment |
|
||||
|-----------|-------------|-------------|-----------------|
|
||||
| v3.2.0 (M3) | 25.7% | 959 | 🔴 High |
|
||||
| v3.3.0 (M4) | 35.5% | 218 | 🔴 High |
|
||||
| v3.4.0 (M5) | 35.5% | 287 | 🔴 High |
|
||||
| v3.5.0 (M6) | 18.1% | 1,130 | 🔴 High |
|
||||
| v3.6.0 (M7) | 32.8% | 332 | 🔴 High |
|
||||
| v3.7.0 (M8) | 41.8% | 608 | 🟡 Medium |
|
||||
| v3.8.0 (M9) | 28.2% | 367 | 🟡 Medium |
|
||||
| v3.9.0 | 37.5% | 40 | 🟢 Low |
|
||||
|
||||
**Automated by CleverAgents Bot**
|
||||
Supervisor: Timeline Update | Agent: timeline-update-pool-supervisor
|
||||
|
||||
@@ -36,27 +36,34 @@ Feature: TUI session export/import (JSON + Markdown)
|
||||
Then the markdown output should contain "**Linked Plans:**"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# CLI export command: JSON-only per spec §1986
|
||||
# CLI export command: --format md flag
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@tdd_issue @tdd_issue_1451
|
||||
Scenario: Export session as JSON (default, no format flag)
|
||||
Scenario: Export session as Markdown to stdout
|
||||
Given there is a mocked session for markdown export
|
||||
When I run session CLI export with --format md and no output file
|
||||
Then the md export CLI result code should be zero
|
||||
And the md export CLI output should include "# Session:"
|
||||
|
||||
Scenario: Export session as Markdown to file
|
||||
Given there is a mocked session for markdown export
|
||||
When I run session CLI export with --format md to a temp file
|
||||
Then the md export CLI result code should be zero
|
||||
And the exported markdown file should exist
|
||||
And the exported markdown file should contain "# Session:"
|
||||
|
||||
@tdd_issue @tdd_issue_4293 @tdd_expected_fail
|
||||
Scenario: Export session as JSON (default format)
|
||||
Given there is a mocked session for markdown export
|
||||
When I run session CLI export with no format flag
|
||||
Then the md export CLI result code should be zero
|
||||
And the md export CLI output should be parseable as JSON
|
||||
|
||||
@tdd_issue @tdd_issue_1451
|
||||
Scenario: Export session rejects --format md flag (CLI is JSON-only per spec)
|
||||
Given there is a mocked session for markdown export
|
||||
When I run session CLI export with --format md and no output file
|
||||
Then the md export CLI result code should be nonzero
|
||||
|
||||
@tdd_issue @tdd_issue_1451
|
||||
Scenario: Export session rejects --format xml flag (CLI is JSON-only per spec)
|
||||
Scenario: Export with invalid format flag returns error
|
||||
Given there is a mocked session for markdown export
|
||||
When I run session CLI export with --format xml
|
||||
Then the md export CLI result code should be nonzero
|
||||
And the md export CLI output should include "Invalid format"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# TUI command router: /session export and /session import
|
||||
@@ -104,3 +111,35 @@ Feature: TUI session export/import (JSON + Markdown)
|
||||
And there is an invalid JSON file for TUI import
|
||||
When I call TUI handle with "session import <invalid_path>" for the import file
|
||||
Then the TUI handle result should contain "Invalid JSON"
|
||||
|
||||
Scenario: TUI session export with --format txt returns success
|
||||
Given a TUI command router with mocked session service for export
|
||||
When I call TUI handle with "session export --format txt" for the current session
|
||||
Then the TUI handle result should contain "Session exported (TXT)"
|
||||
|
||||
Scenario: TUI session export with --format txt to file writes plain text
|
||||
Given a TUI command router with mocked session service for export
|
||||
When I call TUI handle with "session export --format txt /tmp/tui_test_export.txt" for the current session
|
||||
Then the TUI handle result should contain "Session exported to"
|
||||
And the tui exported file "/tmp/tui_test_export.txt" should exist
|
||||
|
||||
Scenario: Plain text export contains session header and messages
|
||||
Given a session with two messages for plain text export
|
||||
When I call as_export_plain_text on the session
|
||||
Then the plain text output should start with "Session:"
|
||||
And the plain text output should contain "USER"
|
||||
And the plain text output should contain "ASSISTANT"
|
||||
And the plain text output should contain "Hello from user"
|
||||
And the plain text output should contain "Hello from assistant"
|
||||
|
||||
Scenario: Plain text export with no messages contains placeholder
|
||||
Given a session with no messages for plain text export
|
||||
When I call as_export_plain_text on the session
|
||||
Then the plain text output should start with "Session:"
|
||||
And the plain text output should contain "(no messages)"
|
||||
|
||||
Scenario: TUI session export error message includes txt format
|
||||
Given a TUI command router with mocked session service for export
|
||||
When I call TUI handle with "session export --format xml" for the current session
|
||||
Then the TUI handle result should contain "Invalid format"
|
||||
And the TUI handle result should contain "txt"
|
||||
|
||||
+94
-91
@@ -1,13 +1,8 @@
|
||||
# yaml-language-server: $schema=https://json.schemastore.org/mkdocs-1.6.json
|
||||
# yaml-language-server: customTags:
|
||||
# - !ENV scalar
|
||||
# - !ENV sequence
|
||||
site_name: CleverAgents Documentation
|
||||
site_description: Documentation for CleverAgents.
|
||||
site_author: CleverThis, Inc.
|
||||
site_url: https://docs.cleverthis.com/cleveragents
|
||||
site_dir: build/site
|
||||
|
||||
nav:
|
||||
- Specification: specification.md
|
||||
- Architecture: architecture.md
|
||||
@@ -52,6 +47,19 @@ nav:
|
||||
- Automation Tracking: development/automation-tracking.md
|
||||
- Custom Sandbox Strategy: development/custom_sandbox_strategy.md
|
||||
- Documentation Writer: development/docs-writer.md
|
||||
- ACP to A2A Migration: development/acp-to-a2a-migration.md
|
||||
- Milestones:
|
||||
- Overview: milestones/index.md
|
||||
- "v3.0.0 \u2014 Minimal Local Source-Code Workflow (M1)":
|
||||
- milestones/v3.0.0/index.md
|
||||
- milestones/v3.0.0/cli-reference.md
|
||||
- milestones/v3.0.0/deep-dive.md
|
||||
- "v3.1.0 \u2014 Actor Compiler + Full LLM Integration (M2)":
|
||||
- milestones/v3.1.0/index.md
|
||||
- milestones/v3.1.0/actor-yaml.md
|
||||
- milestones/v3.1.0/integration.md
|
||||
- milestones/v3.1.0/skills.md
|
||||
- milestones/v3.1.0/quickstart.md
|
||||
- Implementation Timeline: timeline.md
|
||||
- Advanced Concepts (v3.6.0):
|
||||
- Overview & Context Strategies: advanced-concepts/index.md
|
||||
@@ -118,105 +126,100 @@ nav:
|
||||
- ADR-046 TUI Reference and Command System: adr/ADR-046-tui-reference-and-command-system.md
|
||||
- ADR-047 A2A Standard Adoption: adr/ADR-047-acp-standard-adoption.md
|
||||
- ADR-048 Server Application Architecture: adr/ADR-048-server-application-architecture.md
|
||||
|
||||
theme:
|
||||
name: material
|
||||
custom_dir: docs/overrides
|
||||
features:
|
||||
- navigation.tabs
|
||||
- navigation.tabs.sticky
|
||||
- toc.integrate
|
||||
- toc.follow
|
||||
- navigation.tabs
|
||||
- navigation.tabs.sticky
|
||||
- toc.integrate
|
||||
- toc.follow
|
||||
palette:
|
||||
- scheme: default
|
||||
primary: blue
|
||||
accent: cyan
|
||||
toggle:
|
||||
icon: material/brightness-7
|
||||
name: Switch to dark mode
|
||||
- scheme: slate
|
||||
primary: blue
|
||||
accent: cyan
|
||||
toggle:
|
||||
icon: material/brightness-4
|
||||
name: Switch to light mode
|
||||
|
||||
- scheme: default
|
||||
primary: blue
|
||||
accent: cyan
|
||||
toggle:
|
||||
icon: material/brightness-7
|
||||
name: Switch to dark mode
|
||||
- scheme: slate
|
||||
primary: blue
|
||||
accent: cyan
|
||||
toggle:
|
||||
icon: material/brightness-4
|
||||
name: Switch to light mode
|
||||
hooks:
|
||||
- hooks/adr_hooks.py
|
||||
|
||||
- hooks/adr_hooks.py
|
||||
extra_css:
|
||||
- stylesheets/extra.css
|
||||
|
||||
- stylesheets/extra.css
|
||||
extra_javascript:
|
||||
- javascripts/adr-page.js
|
||||
- javascripts/toc-collapse.js
|
||||
- javascripts/diagram-lightbox.js
|
||||
|
||||
- javascripts/adr-page.js
|
||||
- javascripts/toc-collapse.js
|
||||
- javascripts/diagram-lightbox.js
|
||||
extra:
|
||||
adr_tiers:
|
||||
1:
|
||||
title: "Foundational"
|
||||
description: "Structural and technological foundation upon which all other decisions rest."
|
||||
title: Foundational
|
||||
description: Structural and technological foundation upon which all other decisions
|
||||
rest.
|
||||
2:
|
||||
title: "Core Domain"
|
||||
description: "Domain model — entities, lifecycles, and relationships that constitute core logic."
|
||||
title: Core Domain
|
||||
description: "Domain model \u2014 entities, lifecycles, and relationships that\
|
||||
\ constitute core logic."
|
||||
3:
|
||||
title: "Infrastructure and Behavior"
|
||||
description: "Cross-cutting behavioral systems and infrastructure concerns."
|
||||
title: Infrastructure and Behavior
|
||||
description: Cross-cutting behavioral systems and infrastructure concerns.
|
||||
4:
|
||||
title: "Integration and Operations"
|
||||
description: "External integrations, operational interfaces, and deployment concerns."
|
||||
|
||||
title: Integration and Operations
|
||||
description: External integrations, operational interfaces, and deployment concerns.
|
||||
plugins:
|
||||
- search
|
||||
- gen-files:
|
||||
scripts:
|
||||
- docs/gen_ref_pages.py
|
||||
- literate-nav:
|
||||
nav_file: SUMMARY.md
|
||||
- mkdocstrings:
|
||||
handlers:
|
||||
python:
|
||||
paths: [src]
|
||||
options:
|
||||
docstring_style: google
|
||||
docstring_section_style: table
|
||||
show_root_heading: true
|
||||
show_source: true
|
||||
merge_init_into_class: true
|
||||
separate_signature: true
|
||||
show_signature_annotations: true
|
||||
signature_crossrefs: true
|
||||
summary: true
|
||||
extensions:
|
||||
- griffe_pydantic:
|
||||
schema: true
|
||||
- kroki:
|
||||
server_url: https://kroki.qoto.org
|
||||
#server_url: https://kroki.io
|
||||
enable_mermaid: true
|
||||
fence_prefix: kroki-
|
||||
http_method: POST
|
||||
|
||||
- search
|
||||
- gen-files:
|
||||
scripts:
|
||||
- docs/gen_ref_pages.py
|
||||
- literate-nav:
|
||||
nav_file: SUMMARY.md
|
||||
- mkdocstrings:
|
||||
handlers:
|
||||
python:
|
||||
paths:
|
||||
- src
|
||||
options:
|
||||
docstring_style: google
|
||||
docstring_section_style: table
|
||||
show_root_heading: true
|
||||
show_source: true
|
||||
merge_init_into_class: true
|
||||
separate_signature: true
|
||||
show_signature_annotations: true
|
||||
signature_crossrefs: true
|
||||
summary: true
|
||||
extensions:
|
||||
- griffe_pydantic:
|
||||
schema: true
|
||||
- kroki:
|
||||
server_url: https://kroki.qoto.org
|
||||
enable_mermaid: true
|
||||
fence_prefix: kroki-
|
||||
http_method: POST
|
||||
markdown_extensions:
|
||||
- admonition
|
||||
- pymdownx.details
|
||||
- pymdownx.superfences
|
||||
- pymdownx.tabbed:
|
||||
alternate_style: true
|
||||
- pymdownx.critic
|
||||
- pymdownx.caret
|
||||
- pymdownx.keys
|
||||
- pymdownx.mark
|
||||
- pymdownx.tilde
|
||||
- pymdownx.tasklist:
|
||||
custom_checkbox: true
|
||||
- attr_list
|
||||
- def_list
|
||||
- footnotes
|
||||
- md_in_html
|
||||
- tables
|
||||
- codehilite
|
||||
- toc:
|
||||
permalink: true
|
||||
toc_depth: 1-6
|
||||
- admonition
|
||||
- pymdownx.details
|
||||
- pymdownx.superfences
|
||||
- pymdownx.tabbed:
|
||||
alternate_style: true
|
||||
- pymdownx.critic
|
||||
- pymdownx.caret
|
||||
- pymdownx.keys
|
||||
- pymdownx.mark
|
||||
- pymdownx.tilde
|
||||
- pymdownx.tasklist:
|
||||
custom_checkbox: true
|
||||
- attr_list
|
||||
- def_list
|
||||
- footnotes
|
||||
- md_in_html
|
||||
- tables
|
||||
- codehilite
|
||||
- toc:
|
||||
permalink: true
|
||||
toc_depth: 1-6
|
||||
|
||||
+122
-31
@@ -1,48 +1,139 @@
|
||||
*** Settings ***
|
||||
Documentation E2E acceptance test for M2 (v3.1.0): Actor Compiler + Full LLM Integration.
|
||||
Documentation E2E acceptance test for M2 (v3.1.0): Actor Compiler + Full LLM Integration.
|
||||
...
|
||||
... Exercises actor YAML compilation into functional graphs, skill registry,
|
||||
... tool lifecycle, and plan execution with a custom actor using real LLM keys.
|
||||
... Zero mocking — all CLI invocations hit the real CleverAgents binary with
|
||||
... real provider keys.
|
||||
Resource common_e2e.resource
|
||||
Suite Setup E2E Suite Setup
|
||||
... Exercises actor YAML compilation into functional graphs, skill registry,
|
||||
... tool lifecycle, and plan execution with a custom actor using real LLM keys.
|
||||
... Zero mocking — all CLI invocations hit the real CleverAgents binary with
|
||||
... real provider keys.
|
||||
Resource common_e2e.resource
|
||||
Suite Setup E2E Suite Setup
|
||||
Suite Teardown E2E Suite Teardown
|
||||
|
||||
*** Variables ***
|
||||
${ACTOR_NAME} local/m2-e2e-actor
|
||||
${ACTION_NAME} local/m2-e2e-action
|
||||
${RESOURCE_NAME} local/m2-e2e-repo
|
||||
${PROJECT_NAME} local/m2-e2e-project
|
||||
${ACTOR_NAME} local/m2-e2e-actor
|
||||
${ACTION_NAME} local/m2-e2e-action
|
||||
${RESOURCE_NAME} local/m2-e2e-repo
|
||||
${PROJECT_NAME} local/m2-e2e-project
|
||||
|
||||
*** Test Cases ***
|
||||
M2 Full Actor Compiler And LLM Integration
|
||||
[Documentation] End-to-end acceptance test for the M2 milestone.
|
||||
...
|
||||
... Exercises actor YAML compilation, registration via CLI,
|
||||
... resource and project setup, action creation referencing a
|
||||
... custom actor, and the full plan lifecycle (use, execute
|
||||
... strategize, execute, diff, apply) with real LLM API keys.
|
||||
[Tags] E2E tdd_issue tdd_issue_4189 tdd_expected_fail
|
||||
Skip If No LLM Keys
|
||||
# ---- Step 1: Create temp git repo with sample project files ----
|
||||
${repo_dir}= Create Temp Git Repo m2-e2e-repo
|
||||
Create Directory ${repo_dir}${/}src
|
||||
Create File ${repo_dir}${/}src${/}main.py print("hello world")\n
|
||||
${r_add}= Run Process git add . cwd=${repo_dir} timeout=60s on_timeout=kill
|
||||
Should Be Equal As Integers ${r_add.rc} 0 msg=git add failed (rc=${r_add.rc}). Check DEBUG logs above.
|
||||
${r_commit}= Run Process git commit -m Add source files cwd=${repo_dir} timeout=60s on_timeout=kill
|
||||
Should Be Equal As Integers ${r_commit.rc} 0 msg=git commit failed (rc=${r_commit.rc}). Check DEBUG logs above.
|
||||
# Detect the default branch name created by git init
|
||||
${branch_result}= Run Process git rev-parse --abbrev-ref HEAD cwd=${repo_dir} timeout=60s on_timeout=kill
|
||||
${branch}= Strip String ${branch_result.stdout}
|
||||
Log Detected branch: ${branch}
|
||||
[Documentation] End-to-end acceptance test for the M2 milestone.
|
||||
...
|
||||
... Exercises actor YAML compilation, registration via CLI,
|
||||
... resource and project setup, action creation referencing a
|
||||
... custom actor, and the full plan lifecycle (use, execute
|
||||
... strategize, execute, diff, apply) with real LLM API keys.
|
||||
[Tags] E2E
|
||||
Skip If No LLM Keys
|
||||
# ---- Step 1: Create temp git repo with sample project files ----
|
||||
${repo_dir}= Create Temp Git Repo m2-e2e-repo
|
||||
Create Directory ${repo_dir}${/}src
|
||||
Create File ${repo_dir}${/}src${/}main.py print("hello world")\n
|
||||
${r_add}= Run Process git add . cwd=${repo_dir} timeout=60s on_timeout=kill
|
||||
Should Be Equal As Integers ${r_add.rc} 0 msg=git add failed (rc=${r_add.rc}). Check DEBUG logs above.
|
||||
${r_commit}= Run Process git commit -m Add source files cwd=${repo_dir} timeout=60s on_timeout=kill
|
||||
Should Be Equal As Integers ${r_commit.rc} 0 msg=git commit failed (rc=${r_commit.rc}). Check DEBUG logs above.
|
||||
# Detect the default branch name created by git init
|
||||
${branch_result}= Run Process git rev-parse --abbrev-ref HEAD cwd=${repo_dir} timeout=60s on_timeout=kill
|
||||
${branch}= Strip String ${branch_result.stdout}
|
||||
Log Detected branch: ${branch}
|
||||
|
||||
# ---- Resolve LLM actor (OpenAI if available, Anthropic fallback) ----
|
||||
${llm_actor}= Resolve LLM Actor
|
||||
${llm_parts}= Evaluate $llm_actor.split('/', 1)
|
||||
${llm_provider}= Set Variable ${llm_parts}[0]
|
||||
${llm_model}= Set Variable ${llm_parts}[1]
|
||||
|
||||
# ---- Step 2: Create and register custom actor YAML ----
|
||||
${actor_yaml}= Catenate SEPARATOR=\n
|
||||
... name: ${ACTOR_NAME}
|
||||
... type: llm
|
||||
... description: M2 E2E acceptance actor for compiler and LLM integration
|
||||
... version: "1.0"
|
||||
... model: ${llm_model}
|
||||
${actor_yaml_path}= Set Variable ${SUITE_HOME}${/}m2_actor.yaml
|
||||
Create File ${actor_yaml_path} ${actor_yaml}
|
||||
${actor_config}= Catenate SEPARATOR=\n
|
||||
... {
|
||||
... "name": "${ACTOR_NAME}",
|
||||
... "provider": "${llm_provider}",
|
||||
... "model": "${llm_model}",
|
||||
... "options": {"temperature": 0.2}
|
||||
... }
|
||||
${config_path}= Set Variable ${SUITE_HOME}${/}actor_config.json
|
||||
Create File ${config_path} ${actor_config}
|
||||
${r_actor}= Run CleverAgents Command
|
||||
... actor add --config ${config_path} --format plain
|
||||
Should Be Equal As Integers ${r_actor.rc} 0 msg=Actor add failed (rc=${r_actor.rc}). Check DEBUG logs above.
|
||||
Should Not Contain ${r_actor.stdout}${r_actor.stderr} Traceback
|
||||
Log Actor registration: ${r_actor.stdout}
|
||||
|
||||
# ---- Step 3: Register resource and create project ----
|
||||
${r_resource}= Run CleverAgents Command
|
||||
... resource add git-checkout ${RESOURCE_NAME}
|
||||
... --path ${repo_dir} --branch ${branch} --format plain
|
||||
Output Should Contain ${r_resource} ${RESOURCE_NAME}
|
||||
${r_project}= Run CleverAgents Command
|
||||
... project create ${PROJECT_NAME}
|
||||
... --description M2 E2E acceptance project
|
||||
... --resource ${RESOURCE_NAME} --format plain
|
||||
Output Should Contain ${r_project} ${PROJECT_NAME}
|
||||
|
||||
# ---- Step 4: Create action referencing the custom actor ----
|
||||
${action_yaml}= Catenate SEPARATOR=\n
|
||||
... name: ${ACTION_NAME}
|
||||
... description: M2 acceptance test action for actor compiler and LLM integration
|
||||
... definition_of_done: Generate or modify at least one source file
|
||||
... strategy_actor: ${llm_actor}
|
||||
... execution_actor: ${llm_actor}
|
||||
${action_yaml_path}= Set Variable ${SUITE_HOME}${/}action.yaml
|
||||
Create File ${action_yaml_path} ${action_yaml}
|
||||
${r_action}= Run CleverAgents Command
|
||||
... action create --config ${action_yaml_path} --format plain
|
||||
Output Should Contain ${r_action} ${ACTION_NAME}
|
||||
|
||||
# ---- Step 5: Plan use ----
|
||||
${r_use}= Run CleverAgents Command
|
||||
... plan use ${ACTION_NAME} ${PROJECT_NAME} --format plain
|
||||
Should Not Be Empty ${r_use.stdout}
|
||||
${plan_ids}= Get Regexp Matches ${r_use.stdout} [0-9A-Z]{26}
|
||||
Should Not Be Empty ${plan_ids} msg=Expected a ULID plan ID in plan use output
|
||||
${plan_id}= Set Variable ${plan_ids}[0]
|
||||
Log Extracted plan_id: ${plan_id}
|
||||
|
||||
# ---- Step 6: Plan execute — strategize phase ----
|
||||
${r_strategize}= Run CleverAgents Command
|
||||
... plan execute ${plan_id} --format plain
|
||||
... timeout=180s
|
||||
Should Not Contain ${r_strategize.stdout}${r_strategize.stderr} INTERNAL
|
||||
Should Not Contain ${r_strategize.stdout}${r_strategize.stderr} Traceback
|
||||
Log Strategize phase output: ${r_strategize.stdout}
|
||||
|
||||
# ---- Step 7: Plan execute — execute phase ----
|
||||
${r_execute}= Run CleverAgents Command
|
||||
... plan execute ${plan_id} --format plain
|
||||
... timeout=180s
|
||||
Should Not Contain ${r_execute.stdout}${r_execute.stderr} INTERNAL
|
||||
Should Not Contain ${r_execute.stdout}${r_execute.stderr} Traceback
|
||||
Log Execute phase output: ${r_execute.stdout}
|
||||
|
||||
# ---- Step 8: Plan diff ----
|
||||
${r_diff}= Run CleverAgents Command
|
||||
... plan diff ${plan_id} --format plain
|
||||
Should Not Contain ${r_diff.stdout}${r_diff.stderr} INTERNAL
|
||||
Should Not Contain ${r_diff.stdout}${r_diff.stderr} Traceback
|
||||
Log Diff output: ${r_diff.stdout}
|
||||
|
||||
# ---- Step 9: Plan apply ----
|
||||
${r_apply}= Run CleverAgents Command
|
||||
... plan apply --yes ${plan_id} --format plain
|
||||
Should Not Contain ${r_apply.stdout}${r_apply.stderr} INTERNAL
|
||||
Should Not Contain ${r_apply.stdout}${r_apply.stderr} Traceback
|
||||
Log Apply output: ${r_apply.stdout}
|
||||
|
||||
# ---- Step 10: Verify actor compilation and plan integrity ----
|
||||
${r_status}= Run CleverAgents Command
|
||||
... plan status ${plan_id} --format plain
|
||||
Should Not Be Empty ${r_status.stdout}
|
||||
Output Should Contain ${r_status} ${plan_id}
|
||||
Log Final plan status: ${r_status.stdout}
|
||||
|
||||
+26
-15
@@ -5,7 +5,6 @@ Each subcommand is a self-contained check that prints a sentinel on success.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
@@ -315,7 +314,7 @@ def tell_message() -> None:
|
||||
|
||||
|
||||
def export_rich_panels() -> None:
|
||||
"""Test that export to file produces valid JSON and success message."""
|
||||
"""Test that export renders all three spec-required Rich panels."""
|
||||
sid = str(ULID())
|
||||
svc = _setup_service()
|
||||
|
||||
@@ -331,12 +330,20 @@ def export_rich_panels() -> None:
|
||||
try:
|
||||
result = runner.invoke(session_app, ["export", sid, "--output", path])
|
||||
assert result.exit_code == 0, f"exit={result.exit_code}: {result.output}"
|
||||
# CLI export is JSON-only per spec §1986 — no Rich panels for file output
|
||||
assert os.path.exists(path), f"Output file not created at {path}"
|
||||
exported = json.loads(Path(path).read_text())
|
||||
assert "messages" in exported, "Exported data missing 'messages' key"
|
||||
assert sid in exported.get("session_id", "")
|
||||
print("session-cli-export-json-ok")
|
||||
assert "Session Export" in result.output, (
|
||||
f"Missing 'Session Export' panel:\n{result.output}"
|
||||
)
|
||||
assert "Contents" in result.output, (
|
||||
f"Missing 'Contents' panel:\n{result.output}"
|
||||
)
|
||||
assert "Integrity" in result.output, (
|
||||
f"Missing 'Integrity' panel:\n{result.output}"
|
||||
)
|
||||
assert "Export completed" in result.output, (
|
||||
f"Missing 'Export completed':\n{result.output}"
|
||||
)
|
||||
assert sid in result.output, f"Session ID missing from output:\n{result.output}"
|
||||
print("session-cli-export-rich-panels-ok")
|
||||
finally:
|
||||
if os.path.exists(path):
|
||||
os.unlink(path)
|
||||
@@ -344,7 +351,7 @@ def export_rich_panels() -> None:
|
||||
|
||||
|
||||
def export_stdout_rich_panels() -> None:
|
||||
"""Test that stdout export produces valid JSON output."""
|
||||
"""Test that stdout export also renders Rich panels."""
|
||||
sid = str(ULID())
|
||||
svc = _setup_service()
|
||||
|
||||
@@ -356,12 +363,16 @@ def export_stdout_rich_panels() -> None:
|
||||
try:
|
||||
result = runner.invoke(session_app, ["export", sid])
|
||||
assert result.exit_code == 0, f"exit={result.exit_code}: {result.output}"
|
||||
# CLI stdout export is JSON-only per spec §1986
|
||||
data = json.loads(result.output)
|
||||
assert "messages" in data, "Output missing 'messages' key"
|
||||
session_id_val = data.get("session_id", "")
|
||||
assert sid == session_id_val
|
||||
print("session-cli-export-stdout-json-ok")
|
||||
assert "Session Export" in result.output, (
|
||||
f"Missing 'Session Export' panel:\n{result.output}"
|
||||
)
|
||||
assert "(stdout)" in result.output, (
|
||||
f"Missing '(stdout)' indicator:\n{result.output}"
|
||||
)
|
||||
assert "Export completed" in result.output, (
|
||||
f"Missing 'Export completed':\n{result.output}"
|
||||
)
|
||||
print("session-cli-export-stdout-rich-panels-ok")
|
||||
finally:
|
||||
_teardown()
|
||||
|
||||
|
||||
@@ -67,20 +67,20 @@ Session Import Rich Output Panels
|
||||
Should Contain ${result.stdout} session-cli-import-rich-panels-ok
|
||||
|
||||
Session Export Rich Panels
|
||||
[Documentation] Verify that ``session export`` to file produces valid JSON output per spec §1986
|
||||
[Documentation] Verify that ``session export`` renders Session Export, Contents, and Integrity panels
|
||||
${result}= Run Process ${PYTHON} ${HELPER} export-rich-panels cwd=${WORKSPACE}
|
||||
Log ${result.stdout}
|
||||
Log ${result.stderr}
|
||||
Should Be Equal As Integers ${result.rc} 0
|
||||
Should Contain ${result.stdout} session-cli-export-json-ok
|
||||
Should Contain ${result.stdout} session-cli-export-rich-panels-ok
|
||||
|
||||
Session Export Stdout Rich Panels
|
||||
[Documentation] Verify that ``session export`` to stdout produces valid JSON output per spec §1986
|
||||
[Documentation] Verify that ``session export`` to stdout also renders Rich panels
|
||||
${result}= Run Process ${PYTHON} ${HELPER} export-stdout-rich-panels cwd=${WORKSPACE}
|
||||
Log ${result.stdout}
|
||||
Log ${result.stderr}
|
||||
Should Be Equal As Integers ${result.rc} 0
|
||||
Should Contain ${result.stdout} session-cli-export-stdout-json-ok
|
||||
Should Contain ${result.stdout} session-cli-export-stdout-rich-panels-ok
|
||||
|
||||
Session Tell Appends Message
|
||||
[Documentation] Verify that ``session tell`` appends a message
|
||||
|
||||
@@ -16,7 +16,8 @@ from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
import sys
|
||||
import re
|
||||
import threading
|
||||
from collections import OrderedDict
|
||||
from pathlib import Path
|
||||
from typing import Annotated, Any, cast
|
||||
@@ -28,16 +29,23 @@ from rich.table import Table
|
||||
|
||||
from cleveragents.a2a.models import A2aRequest
|
||||
from cleveragents.application.container import get_container
|
||||
from cleveragents.application.services.session_workflow import SessionWorkflow
|
||||
from cleveragents.application.services.strategy_resolution import (
|
||||
build_actor_resolver,
|
||||
)
|
||||
from cleveragents.cli.formatting import OutputFormat, format_output
|
||||
from cleveragents.core.exceptions import DatabaseError
|
||||
from cleveragents.domain.models.core.session import (
|
||||
MessageRole,
|
||||
Session,
|
||||
SessionActorNotConfiguredError,
|
||||
SessionExportError,
|
||||
SessionImportError,
|
||||
SessionMessage,
|
||||
SessionNotFoundError,
|
||||
SessionService,
|
||||
)
|
||||
from cleveragents.providers.registry import ProviderRegistry
|
||||
|
||||
# Create sub-app for session commands
|
||||
app = typer.Typer(help="Manage interactive sessions.")
|
||||
@@ -48,8 +56,16 @@ _log = logging.getLogger(__name__)
|
||||
# Reusable --format option description
|
||||
_FORMAT_HELP = "Output format: json, yaml, plain, table, or rich (default: rich)"
|
||||
|
||||
# MCP logger name constant — used in create() and list_sessions() to suppress
|
||||
# MCP health-check output during structured (non-Rich) CLI output.
|
||||
_MCP_LOGGER_NAME = "cleveragents.mcp"
|
||||
|
||||
# Thread lock for MCP logger level mutations to prevent race conditions when
|
||||
# multiple CLI commands execute concurrently (e.g., in parallel test runners).
|
||||
_mcp_logger_lock = threading.Lock()
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Module-level service accessor (patchable in tests)
|
||||
# Module-level service and workflow accessors (patchable in tests)
|
||||
# ---------------------------------------------------------------------------
|
||||
_service: SessionService | None = None
|
||||
|
||||
@@ -76,11 +92,112 @@ def _reset_session_service() -> None:
|
||||
_service = None
|
||||
|
||||
|
||||
def _build_session_workflow() -> SessionWorkflow:
|
||||
"""Build a :class:`SessionWorkflow` wired to the CLI session service.
|
||||
|
||||
Returns a fully configured workflow using the same ``SessionService``
|
||||
that the CLI commands use. Tests can replace this function or its
|
||||
dependencies by patching ``_service`` (for the service layer) and
|
||||
``_get_provider_registry`` (for the LLM layer).
|
||||
"""
|
||||
service = _get_session_service()
|
||||
provider_registry = _get_provider_registry()
|
||||
actor_resolver = _build_actor_resolver()
|
||||
actor_options_resolver = _build_actor_options_resolver()
|
||||
return SessionWorkflow(
|
||||
session_service=service,
|
||||
provider_registry=provider_registry,
|
||||
actor_resolver=actor_resolver,
|
||||
actor_options_resolver=actor_options_resolver,
|
||||
)
|
||||
|
||||
|
||||
def _get_provider_registry() -> ProviderRegistry | None:
|
||||
"""Attempt to load the provider registry; return ``None`` on failure.
|
||||
|
||||
Isolated in its own function so tests can patch it independently
|
||||
to avoid network / credential errors in CI.
|
||||
"""
|
||||
try:
|
||||
from cleveragents.providers.registry import get_provider_registry
|
||||
|
||||
return get_provider_registry()
|
||||
except (ImportError, RuntimeError, OSError) as exc:
|
||||
_log.warning("Provider registry unavailable: %s", exc)
|
||||
return None
|
||||
|
||||
|
||||
def _build_actor_resolver():
|
||||
"""Build a resolver callable for namespace/name -> provider/model resolution.
|
||||
|
||||
Returns a callable ``(actor_name: str) -> str | None`` that looks up
|
||||
a namespace/name actor reference (e.g. ``"local/my-strategist"``) in
|
||||
the actor registry and returns the ``"provider/model"`` string, or
|
||||
``None`` when the name is already in provider/model format, the actor
|
||||
is unknown, or the registry is unavailable.
|
||||
|
||||
When no DI container is configured the function returns a resolver
|
||||
that always returns ``None`` (graceful degradation).
|
||||
"""
|
||||
try:
|
||||
container = get_container()
|
||||
actor_service = container.actor_service()
|
||||
if actor_service is None:
|
||||
return _null_actor_resolver
|
||||
|
||||
return build_actor_resolver(actor_service)
|
||||
except Exception:
|
||||
_log.warning(
|
||||
"actor_resolver_unavailable",
|
||||
exc_info=True,
|
||||
)
|
||||
return _null_actor_resolver
|
||||
|
||||
|
||||
def _null_actor_resolver(_actor_name: str) -> None:
|
||||
"""Null-object resolver — always returns ``None``."""
|
||||
return None
|
||||
|
||||
|
||||
def _null_actor_options_resolver(_actor_name: str) -> None:
|
||||
"""Null-object options resolver — always returns ``None``."""
|
||||
return None
|
||||
|
||||
|
||||
def _build_actor_options_resolver():
|
||||
"""Build a resolver callable for namespace/name -> actor options dict.
|
||||
|
||||
Returns a callable ``(actor_name: str) -> dict | None`` that looks up
|
||||
a namespace/name actor reference (e.g. ``"local/my-strategist"``) in
|
||||
the actor registry and returns the actor's ``options`` dict from its
|
||||
config blob, or ``None`` when the name is in provider/model format,
|
||||
the actor is unknown, or the registry is unavailable.
|
||||
"""
|
||||
try:
|
||||
container = get_container()
|
||||
actor_service = container.actor_service()
|
||||
if actor_service is None:
|
||||
return _null_actor_options_resolver
|
||||
|
||||
from cleveragents.application.services.strategy_resolution import (
|
||||
build_actor_options_resolver,
|
||||
)
|
||||
|
||||
return build_actor_options_resolver(actor_service)
|
||||
except Exception:
|
||||
_log.warning(
|
||||
"actor_options_resolver_unavailable",
|
||||
exc_info=True,
|
||||
)
|
||||
return _null_actor_options_resolver
|
||||
|
||||
|
||||
def _facade_dispatch(operation: str, params: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Route an operation through the A2A local facade.
|
||||
|
||||
Returns the response data dict on success. Raises on error
|
||||
so that CLI commands can render the appropriate error message.
|
||||
Returns the response data dict on success. Domain errors are mapped
|
||||
back to their original exception types so CLI error handlers work
|
||||
correctly. Unknown / infrastructure errors raise ``RuntimeError``.
|
||||
|
||||
stdout/stderr are redirected during facade construction to prevent
|
||||
structlog or Rich output from polluting CLI output captured by
|
||||
@@ -96,9 +213,25 @@ def _facade_dispatch(operation: str, params: dict[str, Any]) -> dict[str, Any]:
|
||||
from cleveragents.a2a.cli_bootstrap import get_facade
|
||||
|
||||
facade = get_facade()
|
||||
# Wire the CLI's session service and workflow into the facade
|
||||
# so that message/send and message/stream handlers use the same
|
||||
# SessionService and SessionWorkflow as the rest of the CLI.
|
||||
# Tests that patch _get_session_service() and
|
||||
# _build_session_workflow() therefore also control the facade.
|
||||
with contextlib.suppress(Exception):
|
||||
facade.register_service("session_service", _get_session_service())
|
||||
with contextlib.suppress(Exception):
|
||||
facade.register_service("session_workflow", _build_session_workflow())
|
||||
request = A2aRequest(method=operation, params=params)
|
||||
response = facade.dispatch(request)
|
||||
if response.error is not None:
|
||||
# Map A2A error codes back to domain exceptions so CLI error
|
||||
# handlers work correctly when routing through the facade.
|
||||
# Domain exceptions that handlers explicitly re-raise
|
||||
# (SessionNotFoundError, SessionActorNotConfiguredError,
|
||||
# DatabaseError) propagate directly through the facade
|
||||
# dispatch, so they never reach this point. Any error here
|
||||
# is an unknown/infrastructure failure.
|
||||
raise RuntimeError(response.error.message)
|
||||
return dict(response.result or {})
|
||||
|
||||
@@ -203,20 +336,42 @@ def _build_session_create_command(actor: str | None, fmt: str | None) -> str:
|
||||
|
||||
|
||||
def _session_list_dict(sessions: list[Session]) -> dict[str, Any]:
|
||||
"""Build the list output with summary stats."""
|
||||
"""Build the list output with summary stats per spec."""
|
||||
items = []
|
||||
for s in sessions:
|
||||
items.append(
|
||||
{
|
||||
"id": s.session_id,
|
||||
"name": s.name or None,
|
||||
"actor": s.actor_name or "(none)",
|
||||
"messages": s.message_count,
|
||||
"updated": s.updated_at.isoformat(),
|
||||
}
|
||||
)
|
||||
|
||||
# Build summary section per spec
|
||||
total_messages = sum(s.message_count for s in sessions)
|
||||
|
||||
# Find most recent and oldest sessions
|
||||
if sessions:
|
||||
sorted_sessions = sorted(sessions, key=lambda x: x.updated_at, reverse=True)
|
||||
most_recent = sorted_sessions[0].name or sorted_sessions[0].session_id
|
||||
oldest = sorted_sessions[-1].name or sorted_sessions[-1].session_id
|
||||
else:
|
||||
most_recent = None
|
||||
oldest = None
|
||||
|
||||
summary = {
|
||||
"total": len(sessions),
|
||||
"most_recent": most_recent,
|
||||
"oldest": oldest,
|
||||
"total_messages": total_messages,
|
||||
"storage": "0 KB", # Placeholder - actual storage calculation not implemented
|
||||
}
|
||||
|
||||
return {
|
||||
"sessions": items,
|
||||
"total": len(sessions),
|
||||
"summary": summary,
|
||||
}
|
||||
|
||||
|
||||
@@ -245,6 +400,14 @@ def create(
|
||||
agents session create --actor openai/gpt-4
|
||||
agents session create --format json
|
||||
"""
|
||||
# Suppress MCP daemon logger during JSON/YAML output to prevent health check
|
||||
# messages from interfering with structured output.
|
||||
mcp_logger = logging.getLogger(_MCP_LOGGER_NAME)
|
||||
orig_level = mcp_logger.level
|
||||
if fmt not in (OutputFormat.RICH.value, OutputFormat.COLOR.value):
|
||||
with _mcp_logger_lock:
|
||||
mcp_logger.setLevel(logging.CRITICAL)
|
||||
|
||||
try:
|
||||
# Create session through the service, then notify the A2A
|
||||
# facade for protocol compliance and telemetry.
|
||||
@@ -254,13 +417,17 @@ def create(
|
||||
# Notify the facade layer for A2A protocol bookkeeping.
|
||||
# Pass session_id so the facade handler acknowledges the already-
|
||||
# persisted session instead of creating a duplicate (#1141).
|
||||
import contextlib
|
||||
|
||||
with contextlib.suppress(Exception):
|
||||
try:
|
||||
_facade_dispatch(
|
||||
"session.create",
|
||||
{"actor_name": actor or "", "session_id": session.session_id},
|
||||
)
|
||||
except Exception as _exc:
|
||||
_log.warning(
|
||||
"session_create_facade_dispatch_failed",
|
||||
extra={"session_id": session.session_id, "error": str(_exc)},
|
||||
exc_info=True,
|
||||
)
|
||||
|
||||
if fmt not in (OutputFormat.RICH.value, OutputFormat.COLOR.value):
|
||||
payload = _session_create_payload(session)
|
||||
@@ -281,7 +448,35 @@ def create(
|
||||
f"[bold]Namespace:[/bold] {session.namespace}\n"
|
||||
f"[bold]Created:[/bold] {session.created_at.strftime('%Y-%m-%d %H:%M')}"
|
||||
)
|
||||
console.print(Panel(details, title="Session Created", expand=False))
|
||||
console.print(Panel(details, title="Session", expand=False))
|
||||
|
||||
# Settings panel
|
||||
settings_text = (
|
||||
"[yellow]Automation:[/yellow] default\n"
|
||||
"[yellow]Streaming:[/yellow] off\n"
|
||||
"[yellow]Context:[/yellow] default\n"
|
||||
"[yellow]Memory:[/yellow] enabled\n"
|
||||
"[yellow]Max History:[/yellow] 50 turns"
|
||||
)
|
||||
console.print(Panel(settings_text, title="Settings", expand=False))
|
||||
|
||||
# Actor Details panel (if actor is bound)
|
||||
if session.actor_name:
|
||||
try:
|
||||
container = get_container()
|
||||
registry = container.actor_registry()
|
||||
actor_obj = registry.get_actor(session.actor_name)
|
||||
actor_details = (
|
||||
f"[blue]Provider:[/blue] {actor_obj.provider}\n"
|
||||
f"[blue]Model:[/blue] {actor_obj.model}\n"
|
||||
f"[blue]Temperature:[/blue] "
|
||||
f"{getattr(actor_obj, 'temperature', 0.7)}\n"
|
||||
"[blue]Context Window:[/blue] 200K tokens"
|
||||
)
|
||||
console.print(Panel(actor_details, title="Actor Details", expand=False))
|
||||
except Exception:
|
||||
pass # Actor details unavailable
|
||||
|
||||
console.print("[green]✓ OK[/green] Session created")
|
||||
|
||||
except SessionNotFoundError as exc:
|
||||
@@ -294,6 +489,10 @@ def create(
|
||||
"Hint: run 'agents init' to initialise the database."
|
||||
)
|
||||
raise typer.Exit(1) from exc
|
||||
finally:
|
||||
# Restore original MCP logger level
|
||||
with _mcp_logger_lock:
|
||||
mcp_logger.setLevel(orig_level)
|
||||
|
||||
|
||||
@app.command("list")
|
||||
@@ -312,6 +511,14 @@ def list_sessions(
|
||||
agents session list --format json
|
||||
agents session list --format table
|
||||
"""
|
||||
# Suppress MCP daemon logger during JSON/YAML output to prevent health check
|
||||
# messages from interfering with structured output.
|
||||
mcp_logger = logging.getLogger(_MCP_LOGGER_NAME)
|
||||
orig_level = mcp_logger.level
|
||||
if fmt not in (OutputFormat.RICH.value, OutputFormat.COLOR.value):
|
||||
with _mcp_logger_lock:
|
||||
mcp_logger.setLevel(logging.CRITICAL)
|
||||
|
||||
try:
|
||||
service = _get_session_service()
|
||||
sessions = service.list()
|
||||
@@ -322,6 +529,10 @@ def list_sessions(
|
||||
"Hint: run 'agents init' to initialise the database."
|
||||
)
|
||||
raise typer.Exit(1) from exc
|
||||
finally:
|
||||
# Restore original MCP logger level
|
||||
with _mcp_logger_lock:
|
||||
mcp_logger.setLevel(orig_level)
|
||||
|
||||
if not sessions:
|
||||
# For machine-readable formats, always emit a structured empty list
|
||||
@@ -352,29 +563,41 @@ def list_sessions(
|
||||
return
|
||||
|
||||
# Rich table
|
||||
table = Table(title=f"Sessions ({len(sessions)} total)", show_header=True)
|
||||
table = Table(title="Sessions", show_header=True, border_style="blue")
|
||||
table.add_column("ID", style="cyan")
|
||||
table.add_column("Name", style="magenta")
|
||||
table.add_column("Actor", style="blue")
|
||||
table.add_column("Messages", justify="right")
|
||||
table.add_column("Updated", style="green")
|
||||
|
||||
for s in sessions:
|
||||
table.add_row(
|
||||
s.session_id,
|
||||
s.session_id, # Full ULID for copy-paste compatibility with session tell
|
||||
s.name or "(unnamed)",
|
||||
s.actor_name or "(none)",
|
||||
str(s.message_count),
|
||||
s.updated_at.strftime("%Y-%m-%d %H:%M"),
|
||||
)
|
||||
|
||||
console.print(table)
|
||||
console.print()
|
||||
|
||||
# Summary
|
||||
total_msgs = sum(s.message_count for s in sessions)
|
||||
summary = (
|
||||
f"[yellow]Total Sessions:[/yellow] {len(sessions)}\n"
|
||||
f"[blue]Total Messages:[/blue] {total_msgs}"
|
||||
)
|
||||
console.print(Panel(summary, title="Summary", expand=False))
|
||||
# Reuse the summary dict already computed by _session_list_dict to avoid
|
||||
# duplicating the total_msgs / sorted_sessions / most_recent / oldest logic.
|
||||
summary = data["summary"]
|
||||
|
||||
summary_table = Table.grid(padding=(0, 1))
|
||||
summary_table.add_column(style="cyan bold", justify="left")
|
||||
summary_table.add_column(style="white", justify="left")
|
||||
summary_table.add_row("Total:", str(summary["total"]))
|
||||
summary_table.add_row("Most Recent:", summary["most_recent"] or "")
|
||||
summary_table.add_row("Oldest:", summary["oldest"] or "")
|
||||
summary_table.add_row("Total Messages:", str(summary["total_messages"]))
|
||||
summary_table.add_row("Storage:", summary["storage"])
|
||||
|
||||
console.print(Panel(summary_table, title="Summary", border_style="blue"))
|
||||
console.print()
|
||||
console.print(f"[green]✓ OK[/green] {len(sessions)} sessions listed")
|
||||
|
||||
|
||||
@app.command()
|
||||
@@ -411,45 +634,50 @@ def show(
|
||||
)
|
||||
return
|
||||
|
||||
# Session summary panel
|
||||
# Session summary panel — field order per spec: ID, Actor, Messages,
|
||||
# Created, Updated, Automation (docs/specification.md §agents session show)
|
||||
details = (
|
||||
f"[bold]Session ID:[/bold] {session.session_id}\n"
|
||||
f"[bold]ID:[/bold] {session.session_id}\n"
|
||||
f"[bold]Actor:[/bold] {session.actor_name or '(none)'}\n"
|
||||
f"[bold]Namespace:[/bold] {session.namespace}\n"
|
||||
f"[bold]Messages:[/bold] {session.message_count}\n"
|
||||
f"[bold]Created:[/bold] {session.created_at.strftime('%Y-%m-%d %H:%M')}\n"
|
||||
f"[bold]Updated:[/bold] {session.updated_at.strftime('%Y-%m-%d %H:%M')}"
|
||||
f"[bold]Updated:[/bold] {session.updated_at.strftime('%Y-%m-%d %H:%M')}\n"
|
||||
f"[bold]Automation:[/bold] {session.automation or '(none)'}"
|
||||
)
|
||||
console.print(Panel(details, title="Session Details", expand=False))
|
||||
console.print(Panel(details, title="Session Summary", expand=False))
|
||||
|
||||
# Recent messages
|
||||
if session.messages:
|
||||
recent = session.messages[-5:]
|
||||
msg_table = Table(title="Recent Messages", show_header=True)
|
||||
msg_table.add_column("Role", style="cyan")
|
||||
msg_table.add_column("Content")
|
||||
msg_table.add_column("Timestamp", style="dim")
|
||||
msg_table.add_column("Text")
|
||||
for msg in recent:
|
||||
content = msg.content
|
||||
if len(content) > 80:
|
||||
content = content[:77] + "..."
|
||||
msg_table.add_row(
|
||||
msg.role.value,
|
||||
content,
|
||||
msg.timestamp.strftime("%H:%M:%S"),
|
||||
)
|
||||
text = msg.content
|
||||
if len(text) > 80:
|
||||
text = text[:77] + "..."
|
||||
msg_table.add_row(msg.role.value, text)
|
||||
console.print(msg_table)
|
||||
|
||||
# Linked plans
|
||||
if session.linked_plan_ids:
|
||||
# Linked plans — spec requires Plan ID / Phase / State columns
|
||||
if session.linked_plans:
|
||||
plan_table = Table(title="Linked Plans", show_header=True)
|
||||
plan_table.add_column("Plan ID", style="cyan")
|
||||
plan_table.add_column("Phase")
|
||||
plan_table.add_column("State")
|
||||
for lp in session.linked_plans:
|
||||
plan_table.add_row(lp.plan_id, lp.phase, lp.state)
|
||||
console.print(Panel(plan_table, title="Linked Plans", expand=False))
|
||||
elif session.linked_plan_ids:
|
||||
# Fallback: only flat IDs available
|
||||
plan_text = "\n".join(f" • {pid}" for pid in session.linked_plan_ids)
|
||||
console.print(Panel(plan_text, title="Linked Plans", expand=False))
|
||||
|
||||
# Token usage
|
||||
tu = session.token_usage
|
||||
usage_text = (
|
||||
f"[blue]Input Tokens:[/blue] {tu.input_tokens}\n"
|
||||
f"[blue]Output Tokens:[/blue] {tu.output_tokens}\n"
|
||||
f"[blue]Input Tokens:[/blue] {tu.input_tokens:,}\n"
|
||||
f"[blue]Output Tokens:[/blue] {tu.output_tokens:,}\n"
|
||||
f"[yellow]Estimated Cost:[/yellow] ${tu.estimated_cost:.4f}"
|
||||
)
|
||||
console.print(Panel(usage_text, title="Token Usage", expand=False))
|
||||
@@ -476,6 +704,8 @@ def show(
|
||||
)
|
||||
console.print(Panel(budget_text, title="Cost Budget", expand=False))
|
||||
|
||||
console.print("[green bold]✓ OK[/green bold] Session details loaded")
|
||||
|
||||
except SessionNotFoundError as exc:
|
||||
console.print(f"[red]Session not found:[/red] {session_id}")
|
||||
raise typer.Exit(1) from exc
|
||||
@@ -498,6 +728,10 @@ def delete(
|
||||
bool,
|
||||
typer.Option("--yes", "-y", help="Skip confirmation prompt"),
|
||||
] = False,
|
||||
fmt: Annotated[
|
||||
OutputFormat,
|
||||
typer.Option("--format", help=_FORMAT_HELP),
|
||||
] = OutputFormat.RICH,
|
||||
) -> None:
|
||||
"""Delete a session permanently.
|
||||
|
||||
@@ -510,8 +744,9 @@ def delete(
|
||||
try:
|
||||
service = _get_session_service()
|
||||
|
||||
# Verify session exists before prompting
|
||||
service.get(session_id)
|
||||
# Verify session exists before prompting and capture message count
|
||||
session_to_delete = service.get(session_id)
|
||||
message_count = session_to_delete.message_count
|
||||
|
||||
if not yes:
|
||||
confirm = typer.confirm(f"Delete session {session_id}?", default=False)
|
||||
@@ -520,7 +755,53 @@ def delete(
|
||||
raise typer.Abort()
|
||||
|
||||
service.delete(session_id)
|
||||
console.print(f"[green]✓ OK[/green] Session {session_id} deleted")
|
||||
|
||||
# Rich output: render Deletion Summary and Cleanup panels
|
||||
if fmt == OutputFormat.RICH:
|
||||
# Deletion Summary panel
|
||||
summary_table = Table.grid(padding=(0, 1))
|
||||
summary_table.add_column(style="cyan bold", justify="left")
|
||||
summary_table.add_column(style="white", justify="left")
|
||||
summary_table.add_row("Session:", session_id)
|
||||
summary_table.add_row("ID:", session_id)
|
||||
summary_table.add_row("Messages:", f"{message_count} removed")
|
||||
summary_table.add_row("Storage:", "0 KB freed")
|
||||
summary_table.add_row("Plans Orphaned:", "0")
|
||||
|
||||
console.print(
|
||||
Panel(summary_table, title="Deletion Summary", border_style="blue")
|
||||
)
|
||||
console.print()
|
||||
|
||||
# Cleanup panel
|
||||
cleanup_table = Table.grid(padding=(0, 1))
|
||||
cleanup_table.add_column(style="cyan bold", justify="left")
|
||||
cleanup_table.add_column(style="white", justify="left")
|
||||
cleanup_table.add_row("Backups:", "none")
|
||||
cleanup_table.add_row("Logs:", "preserved")
|
||||
cleanup_table.add_row("Context:", "cleared")
|
||||
cleanup_table.add_row("Checkpoints:", "none")
|
||||
|
||||
console.print(Panel(cleanup_table, title="Cleanup", border_style="blue"))
|
||||
console.print()
|
||||
console.print("[green]✓ OK[/green] Session deleted")
|
||||
elif fmt == OutputFormat.COLOR:
|
||||
# Color format: human-readable Rich-styled line, no envelope.
|
||||
console.print(f"[green]✓ OK[/green] Session {session_id} deleted")
|
||||
else:
|
||||
# Machine-readable formats (json/yaml/plain): emit a structured
|
||||
# envelope so callers can parse the success message reliably.
|
||||
typer.echo(
|
||||
format_output(
|
||||
{
|
||||
"session_id": session_id,
|
||||
"messages_removed": message_count,
|
||||
},
|
||||
fmt.value,
|
||||
command=f"agents session delete {session_id}",
|
||||
messages=[{"level": "ok", "text": "Session deleted"}],
|
||||
)
|
||||
)
|
||||
|
||||
except SessionNotFoundError as exc:
|
||||
console.print(f"[red]Session not found:[/red] {session_id}")
|
||||
@@ -549,16 +830,12 @@ def export_session(
|
||||
typer.Option("--force", help="Overwrite existing output file"),
|
||||
] = False,
|
||||
fmt: Annotated[
|
||||
str | None,
|
||||
str,
|
||||
typer.Option(
|
||||
"--format",
|
||||
"-f",
|
||||
help=(
|
||||
"Output data format (JSON only). Use --output-format for CLI "
|
||||
"presentation style (rich/json/yaml/plain)."
|
||||
),
|
||||
help="Export format: json (default) or md (Markdown transcript)",
|
||||
),
|
||||
] = None,
|
||||
] = "json",
|
||||
output_format: Annotated[
|
||||
str | None,
|
||||
typer.Option(
|
||||
@@ -571,33 +848,55 @@ def export_session(
|
||||
),
|
||||
] = None,
|
||||
) -> None:
|
||||
"""Export a session as a portable JSON file.
|
||||
"""Export a session as JSON or Markdown.
|
||||
|
||||
Writes the full session data to a file or stdout in JSON format.
|
||||
The exported file can be re-imported with ``agents session import``.
|
||||
Writes the full session data to a file or stdout. The default format is
|
||||
JSON (canonical, importable). Use ``--format md`` for a human-readable
|
||||
Markdown transcript (lossy — cannot be re-imported).
|
||||
|
||||
Per spec §1986, the CLI export command produces JSON only.
|
||||
Markdown export is available via the TUI ``/session:export --format md`` command.
|
||||
On success, three Rich panels are displayed: "Session Export" (session ID,
|
||||
output path, message count, file size, format), "Contents" (messages, plan
|
||||
references, metadata keys, actor config, schema version), and "Integrity"
|
||||
(checksum, encrypted flag).
|
||||
|
||||
Examples:
|
||||
agents session export 01HXYZ...
|
||||
agents session export 01HXYZ... -o session.json
|
||||
agents session export 01HXYZ... -o session.json --force
|
||||
agents session export 01HXYZ... --format md -o session.md
|
||||
agents session export 01HXYZ... --output-format json
|
||||
"""
|
||||
# CLI export produces JSON data only — reject incompatible data formats.
|
||||
if fmt is not None and fmt != "json":
|
||||
console.print(
|
||||
f"[red]Error:[/red] Invalid format {fmt!r}. "
|
||||
"CLI export supports JSON only. Use the TUI for Markdown export."
|
||||
)
|
||||
if fmt not in ("json", "md"):
|
||||
console.print(f"[red]Invalid format:[/red] {fmt!r}. Use 'json' or 'md'.")
|
||||
raise typer.Exit(1)
|
||||
structured_output = output_format in ("json", "yaml", "plain")
|
||||
|
||||
try:
|
||||
service = _get_session_service()
|
||||
json_data: dict[str, Any] = {}
|
||||
|
||||
data = service.export_session(session_id)
|
||||
content = json.dumps(data, indent=2, default=str)
|
||||
if fmt == "md":
|
||||
# Markdown export: load full session with messages
|
||||
session = service.get(session_id)
|
||||
# Load messages via export_session to populate session.messages
|
||||
json_data = service.export_session(session_id)
|
||||
messages = [
|
||||
SessionMessage(
|
||||
message_id=m["message_id"],
|
||||
role=MessageRole(m["role"]),
|
||||
content=m["content"],
|
||||
sequence=m["sequence"],
|
||||
timestamp=m["timestamp"],
|
||||
metadata=m.get("metadata", {}),
|
||||
tool_call_id=m.get("tool_call_id"),
|
||||
)
|
||||
for m in json_data.get("messages", [])
|
||||
]
|
||||
session.messages = messages
|
||||
content = session.as_export_markdown()
|
||||
else:
|
||||
json_data = service.export_session(session_id)
|
||||
content = json.dumps(json_data, indent=2, default=str)
|
||||
|
||||
if output is not None:
|
||||
if output.exists() and not force:
|
||||
@@ -609,7 +908,6 @@ def export_session(
|
||||
# Create parent directories if needed
|
||||
output.parent.mkdir(parents=True, exist_ok=True)
|
||||
output.write_text(content, encoding="utf-8")
|
||||
console.print(f"[green]✓ OK[/green] Session exported to {output}")
|
||||
elif not structured_output:
|
||||
# Structured output formats suppress the raw content emission so
|
||||
# the envelope is the only thing on stdout (and remains valid JSON).
|
||||
@@ -620,9 +918,9 @@ def export_session(
|
||||
envelope_data: dict[str, Any] = {
|
||||
"session_id": session_id,
|
||||
"output": str(output) if output is not None else None,
|
||||
"format": output_format,
|
||||
"messages_exported": len(data.get("messages", [])),
|
||||
"schema_version": data.get("schema_version", "v1"),
|
||||
"format": fmt,
|
||||
"messages_exported": len(json_data.get("messages", [])),
|
||||
"schema_version": json_data.get("schema_version", "v1"),
|
||||
}
|
||||
typer.echo(
|
||||
format_output(
|
||||
@@ -632,6 +930,15 @@ def export_session(
|
||||
messages=[{"level": "ok", "text": "Export completed"}],
|
||||
)
|
||||
)
|
||||
else:
|
||||
# Render Rich panels for both file and stdout export paths
|
||||
_render_export_panels(
|
||||
session_id=session_id,
|
||||
output=output,
|
||||
content=content,
|
||||
export_data=json_data,
|
||||
fmt=fmt,
|
||||
)
|
||||
|
||||
except SessionNotFoundError as exc:
|
||||
console.print(f"[red]Session not found:[/red] {session_id}")
|
||||
@@ -648,6 +955,83 @@ def export_session(
|
||||
raise typer.Exit(1) from exc
|
||||
|
||||
|
||||
def _render_export_panels(
|
||||
*,
|
||||
session_id: str,
|
||||
output: Path | None,
|
||||
content: str,
|
||||
export_data: dict[str, Any],
|
||||
fmt: str,
|
||||
) -> None:
|
||||
"""Render the three spec-required Rich panels for ``agents session export``.
|
||||
|
||||
Displays:
|
||||
- "Session Export" panel: session ID, output path, message count, size, format
|
||||
- "Contents" panel: messages, plan references, metadata keys, actor config,
|
||||
schema version
|
||||
- "Integrity" panel: checksum, encrypted flag
|
||||
- ``✓ OK Export completed`` success line
|
||||
"""
|
||||
# Compute file size from content
|
||||
size_bytes = len(content.encode("utf-8"))
|
||||
if size_bytes < 1024:
|
||||
size_str = f"{size_bytes} B"
|
||||
elif size_bytes < 1024 * 1024:
|
||||
size_str = f"{size_bytes // 1024} KB"
|
||||
else:
|
||||
size_str = f"{size_bytes // (1024 * 1024)} MB"
|
||||
|
||||
# Derive display values from export data (JSON format) or defaults (md)
|
||||
message_count = len(export_data.get("messages", []))
|
||||
plan_refs = len(export_data.get("linked_plan_ids", []))
|
||||
metadata_keys = len(export_data.get("metadata", {}))
|
||||
actor_config = "included" if export_data.get("actor_name") else "none"
|
||||
schema_version = export_data.get("schema_version", "v1")
|
||||
checksum_raw = export_data.get("checksum", "")
|
||||
checksum_display = (
|
||||
f"sha256:{checksum_raw[:4]}...{checksum_raw[-4:]}"
|
||||
if len(checksum_raw) >= 8
|
||||
else checksum_raw or "n/a"
|
||||
)
|
||||
output_display = str(output) if output is not None else "(stdout)"
|
||||
format_display = "JSON" if fmt == "json" else "Markdown"
|
||||
|
||||
# Session Export panel
|
||||
export_table = Table.grid(padding=(0, 1))
|
||||
export_table.add_column(style="cyan bold", justify="left")
|
||||
export_table.add_column(style="white", justify="left")
|
||||
export_table.add_row("Session:", session_id)
|
||||
export_table.add_row("Output:", output_display)
|
||||
export_table.add_row("Messages:", str(message_count))
|
||||
export_table.add_row("Size:", size_str)
|
||||
export_table.add_row("Format:", format_display)
|
||||
console.print(Panel(export_table, title="Session Export", border_style="blue"))
|
||||
console.print()
|
||||
|
||||
# Contents panel
|
||||
contents_table = Table.grid(padding=(0, 1))
|
||||
contents_table.add_column(style="blue bold", justify="left")
|
||||
contents_table.add_column(style="white", justify="left")
|
||||
contents_table.add_row("Messages:", str(message_count))
|
||||
contents_table.add_row("Plan References:", str(plan_refs))
|
||||
contents_table.add_row("Metadata Keys:", str(metadata_keys))
|
||||
contents_table.add_row("Actor Config:", actor_config)
|
||||
contents_table.add_row("Schema Version:", str(schema_version))
|
||||
console.print(Panel(contents_table, title="Contents", border_style="blue"))
|
||||
console.print()
|
||||
|
||||
# Integrity panel
|
||||
integrity_table = Table.grid(padding=(0, 1))
|
||||
integrity_table.add_column(style="magenta bold", justify="left")
|
||||
integrity_table.add_column(style="white", justify="left")
|
||||
integrity_table.add_row("Checksum:", checksum_display)
|
||||
integrity_table.add_row("Encrypted:", "no")
|
||||
console.print(Panel(integrity_table, title="Integrity", border_style="blue"))
|
||||
console.print()
|
||||
|
||||
console.print("[green]✓ OK[/green] Export completed")
|
||||
|
||||
|
||||
@app.command("import")
|
||||
def import_session(
|
||||
input_file: Annotated[
|
||||
@@ -681,6 +1065,8 @@ def import_session(
|
||||
|
||||
try:
|
||||
service = _get_session_service()
|
||||
schema_version = data.get("schema_version", "unknown")
|
||||
actor_name = data.get("actor_name")
|
||||
session = service.import_session(data)
|
||||
|
||||
if fmt not in (OutputFormat.RICH.value, OutputFormat.COLOR.value):
|
||||
@@ -688,8 +1074,8 @@ def import_session(
|
||||
"session_id": session.session_id,
|
||||
"input": str(input_file),
|
||||
"message_count": session.message_count,
|
||||
"schema_version": data.get("schema_version", "v1"),
|
||||
"actor_ref": "resolved" if data.get("actor_name") else "none",
|
||||
"schema_version": schema_version,
|
||||
"actor_ref": "resolved" if actor_name else "none",
|
||||
}
|
||||
typer.echo(
|
||||
format_output(
|
||||
@@ -701,14 +1087,29 @@ def import_session(
|
||||
)
|
||||
return
|
||||
|
||||
details = (
|
||||
# Session Import panel
|
||||
session_details = (
|
||||
f"[bold]Input:[/bold] {input_file}\n"
|
||||
f"[bold]Session ID:[/bold] {session.session_id}\n"
|
||||
f"[bold]Actor:[/bold] {session.actor_name or '(none)'}\n"
|
||||
f"[bold]Messages:[/bold] {session.message_count}\n"
|
||||
f"[bold]Namespace:[/bold] {session.namespace}"
|
||||
f"[bold]Schema:[/bold] {schema_version}"
|
||||
)
|
||||
console.print(Panel(details, title="Session Imported", expand=False))
|
||||
console.print("[green]✓ OK[/green] Session imported")
|
||||
console.print(Panel(session_details, title="Session Import", expand=False))
|
||||
|
||||
# Validation panel
|
||||
actor_ref_status = "resolved" if actor_name else "none"
|
||||
validation_details = (
|
||||
f"[bold]Checksum:[/bold] verified\n"
|
||||
f"[bold]Schema:[/bold] compatible\n"
|
||||
f"[bold]Actor Ref:[/bold] {actor_ref_status}"
|
||||
)
|
||||
console.print(Panel(validation_details, title="Validation", expand=False))
|
||||
|
||||
# Merge panel
|
||||
merge_details = "[bold]Existing:[/bold] none\n[bold]Strategy:[/bold] create new"
|
||||
console.print(Panel(merge_details, title="Merge", expand=False))
|
||||
|
||||
console.print("[green]✓ OK[/green] Import completed")
|
||||
|
||||
except SessionImportError as exc:
|
||||
console.print(f"[red]Import error:[/red] {exc}")
|
||||
@@ -740,51 +1141,38 @@ def tell(
|
||||
bool,
|
||||
typer.Option("--stream", help="Stream response in real-time"),
|
||||
] = False,
|
||||
fmt: Annotated[
|
||||
OutputFormat,
|
||||
typer.Option(
|
||||
"--format",
|
||||
"-f",
|
||||
help=_FORMAT_HELP,
|
||||
),
|
||||
] = OutputFormat.RICH,
|
||||
) -> None:
|
||||
"""Send a message to a session.
|
||||
|
||||
Appends a user message and generates an assistant response. For M3, the
|
||||
actor execution is stubbed — the assistant echoes an acknowledgement.
|
||||
Appends a user message and invokes the session's bound orchestrator actor
|
||||
(or ``--actor`` override) via the A2A ``message/send`` operation.
|
||||
The actor's real response is persisted and token usage is tracked.
|
||||
|
||||
Examples:
|
||||
agents session tell --session 01HXYZ... "Hello, world"
|
||||
agents session tell --session 01HXYZ... --actor openai/gpt-4 "Plan a feature"
|
||||
agents session tell --session 01HXYZ... --stream "Build tests"
|
||||
agents session tell --session 01HXYZ... --format json "Hello"
|
||||
"""
|
||||
try:
|
||||
service = _get_session_service()
|
||||
|
||||
# Append user message
|
||||
service.append_message(
|
||||
_do_session_tell(
|
||||
session_id=session_id,
|
||||
role=MessageRole.USER,
|
||||
content=prompt,
|
||||
prompt=prompt,
|
||||
actor_override=actor,
|
||||
stream=stream,
|
||||
fmt=fmt,
|
||||
)
|
||||
|
||||
# Stub actor execution: generate simple assistant response
|
||||
assistant_content = (
|
||||
f"Acknowledged: {prompt[:100]}"
|
||||
if not actor
|
||||
else f"[{actor}] Acknowledged: {prompt[:100]}"
|
||||
)
|
||||
service.append_message(
|
||||
session_id=session_id,
|
||||
role=MessageRole.ASSISTANT,
|
||||
content=assistant_content,
|
||||
)
|
||||
|
||||
if stream:
|
||||
# Simulate streaming by printing character by character
|
||||
for char in assistant_content:
|
||||
sys.stdout.write(char)
|
||||
sys.stdout.flush()
|
||||
sys.stdout.write("\n")
|
||||
else:
|
||||
from rich.markup import escape
|
||||
|
||||
console.print(f"[dim]user:[/dim] {escape(prompt)}")
|
||||
console.print(f"[cyan]assistant:[/cyan] {escape(assistant_content)}")
|
||||
|
||||
except SessionActorNotConfiguredError as exc:
|
||||
console.print(f"[red]Error:[/red] {exc}")
|
||||
raise typer.Exit(1) from exc
|
||||
except SessionNotFoundError as exc:
|
||||
console.print(f"[red]Session not found:[/red] {session_id}")
|
||||
raise typer.Exit(1) from exc
|
||||
@@ -795,3 +1183,95 @@ def tell(
|
||||
"Hint: run 'agents init' to initialise the database."
|
||||
)
|
||||
raise typer.Exit(1) from exc
|
||||
except Exception as exc:
|
||||
if not isinstance(exc, typer.Exit):
|
||||
_log.exception("session tell failed with unexpected error")
|
||||
console.print(
|
||||
"[red]Error:[/red] An unexpected error occurred. "
|
||||
"Check the logs for details."
|
||||
)
|
||||
raise typer.Exit(1) from exc
|
||||
raise
|
||||
|
||||
|
||||
def _do_session_tell(
|
||||
session_id: str,
|
||||
prompt: str,
|
||||
actor_override: str | None,
|
||||
stream: bool,
|
||||
fmt: OutputFormat,
|
||||
) -> None:
|
||||
"""Core implementation of ``session tell``, extracted for testability.
|
||||
|
||||
Both streaming and non-streaming paths route through
|
||||
:class:`~cleveragents.a2a.facade.A2aLocalFacade` per the spec's
|
||||
A2A protocol mapping (message/send, message/stream; addresses C3, C5).
|
||||
"""
|
||||
# Validate --actor format before passing to the workflow (m8).
|
||||
if actor_override is not None and not re.match(
|
||||
r"^[a-z0-9][a-z0-9_-]*/[a-z0-9][a-z0-9_-]*$",
|
||||
actor_override,
|
||||
):
|
||||
console.print(
|
||||
f"[red]Error:[/red] Invalid actor name {actor_override!r}. "
|
||||
"Expected format: 'namespace/name' "
|
||||
"(e.g. 'openai/gpt-4')."
|
||||
)
|
||||
raise typer.Exit(1)
|
||||
|
||||
# Route through A2aLocalFacade per the spec's A2A protocol mapping.
|
||||
# message/send → SessionWorkflow.tell() (non-streaming)
|
||||
# message/stream → falls back to non-streaming with streamed=false
|
||||
# (true SSE-based streaming is deferred; acceptable per the spec).
|
||||
operation = "message/stream" if stream else "message/send"
|
||||
result_dict = _facade_dispatch(
|
||||
operation,
|
||||
{
|
||||
"session_id": session_id,
|
||||
"message": prompt,
|
||||
"actor": actor_override,
|
||||
},
|
||||
)
|
||||
|
||||
assistant_content = result_dict.get("assistant_message", "")
|
||||
usage = result_dict.get("usage", {})
|
||||
|
||||
if fmt not in (OutputFormat.RICH.value, OutputFormat.COLOR.value):
|
||||
data: dict[str, object] = {
|
||||
"session_id": session_id,
|
||||
"user_message": prompt,
|
||||
"assistant_message": assistant_content,
|
||||
"usage": usage,
|
||||
}
|
||||
typer.echo(format_output(data, fmt))
|
||||
return
|
||||
|
||||
from rich.markup import escape
|
||||
|
||||
console.print(f"[dim]user:[/dim] {escape(prompt)}")
|
||||
console.print(f"[cyan]assistant:[/cyan] {escape(assistant_content)}")
|
||||
_print_usage_panel(
|
||||
input_tokens=int(usage.get("input_tokens", 0)),
|
||||
output_tokens=int(usage.get("output_tokens", 0)),
|
||||
cost=float(usage.get("cost_usd", 0.0)),
|
||||
duration_ms=float(usage.get("duration_ms", 0.0)),
|
||||
tool_calls=int(usage.get("tool_calls", 0)),
|
||||
)
|
||||
|
||||
|
||||
def _print_usage_panel(
|
||||
input_tokens: int,
|
||||
output_tokens: int,
|
||||
cost: float,
|
||||
duration_ms: float,
|
||||
tool_calls: int,
|
||||
) -> None:
|
||||
"""Render a Rich Usage panel summarising token and cost metrics."""
|
||||
lines = [
|
||||
f"[bold]Input tokens:[/bold] {input_tokens}",
|
||||
f"[bold]Output tokens:[/bold] {output_tokens}",
|
||||
f"[bold]Est. cost:[/bold] ${cost:.6f}",
|
||||
f"[bold]Duration:[/bold] {duration_ms / 1000:.1f}s",
|
||||
f"[bold]Tool calls:[/bold] {tool_calls}",
|
||||
]
|
||||
console.print(Panel("\n".join(lines), title="Usage", expand=False))
|
||||
|
||||
Reference in New Issue
Block a user