Project Framework
August 26, 2026 · View on GitHub
A project tree is an implementation result, not the starting design. Plan the real owners and consumers first, then create the smallest file layout that carries those boundaries.
For a complete runnable example, see the Agently-Skills
skills/agently/assets/full-stack-reference
asset. It intentionally demonstrates several optional boundaries at once. Copy
it selectively; it is not a mandatory scaffold for a small application.
Plan topology before files
For every non-trivial linear, branching, concurrent, or looped model application, record four ledgers before creating modules:
- Owner and invariant ledger: which model, host, Action, flow, storage, transport, or human owner makes each decision, and what must remain true.
- Planned node ledger: each logical ModelRequest or host stage, its input, exact output schema, evidence boundary, lifecycle, and split reason.
- Planned edge ledger: each value, state, signal, effect, and user projection, including its producer, validation or transformation, and consumer.
- Production-necessity ledger: why every node and field exists, who consumes it, its visibility and retention, failure behavior, and whether a claimed quality benefit is hypothetical, observed, or A/B verified.
A node in the plan is not automatically a Python file. Several small host validations can stay beside their owning Chunk; one reusable external contract may deserve a module. Choose files from ownership boundaries, not diagram-box count.
Runtime graphs, events, traces, and artifacts validate the planned topology; they do not replace planning. A graph can show activation without proving that the right field reached its consumer.
For a key handoff with an independent consumer, parallel-development benefit, or local-validation value, consider providing small replaceable reference data before the real producer is complete. This lets downstream work develop against the information it actually consumes and gives the producer a concrete target. The form is project-defined: it may be a DTO, structured example, JSON fixture, event payload, file artifact, or a domain case plus topology data.
A model may simulate an expert to draft such material, but the draft remains simulated until confirmed by a developer, domain fact, interface contract, or observed run. The completed producer must prove that its real output can replace the reference data, and the consumer must not overfit one sample's wording or incidental values. Do not add a fixed packet schema or an extra handoff when no independent development, replay, validation, or fault-localization value exists.
Keep Intermediate Output Locally Correct And Progressive
A non-terminal model output or assistant turn does not need to complete the whole task. Define its current stage, subject, next consumer, and local acceptance boundary. Within that scope it must satisfy its schema and hard invariants, stay grounded in supplied facts, and avoid known material logical or domain errors.
The output should create observable progress: advance one state, narrow an uncertainty, select one actionable next step, or produce information consumed by a later stage. Label assumptions, provisional conclusions, unknowns, and deferred work instead of presenting them as finished facts. Do not add speculative answers for unrelated future stages merely to look complete.
Progress does not excuse a broken local contract or unsafe action. Conversely, do not reject a sound bounded contribution only because later work remains. Terminal results, whole-task completion claims, and irreversible effects still require the full terminal acceptance contract. For example, a schematic tutor may recommend and locally validate one minimal circuit action without finishing the complete schematic in that turn.
Start with the minimum honest layout
One request family
project/
├── app.py
├── SETTINGS.yaml
├── prompts/
│ └── request.yaml
└── tests/
Keep the request in app.py when it is small and has one consumer. Add a
request module only when it owns a reusable Prompt/output contract or meaningful
host validation. Do not add TriggerFlow, services/, domain/, tools/, or
empty packages for appearance.
Use the model for semantic work such as intent recognition, routing, planning, trade-offs, response generation, and quality judgment. Keep schema and enum validation, trusted-key membership, authorization, hard policy, lifecycle, canonical identity reconstruction, and side effects host-owned. Model participation does not require a separate ModelRequest for every semantic step.
Stable multi-stage workflow
project/
├── app.py
├── SETTINGS.yaml
├── TOPOLOGY.md
├── prompts/
├── workflows/
│ ├── main_flow.py
│ └── chunks/
└── tests/
Developer-owned stable topology belongs to TriggerFlow. TOPOLOGY.md carries
the four ledgers; main_flow.py owns graph and execution lifecycle; each
justified Chunk owns one independently observable business stage. Do not add a
separate join Chunk when for_each(...).end_for_each() already returns the
joined list and no transformation, partial-failure, or policy boundary exists.
Map ordered and independent work before implementation. Use async APIs and
bounded concurrency for independent stages. Serial execution is appropriate
only for a real data dependency, ordering rule, side-effect safety boundary, or
external capacity limit. Put pressure controls with their real owners: host
admission, TriggerFlow execution, batch/for_each, model scheduling, client
pools, or blocking-code thread pools.
Submitted or model-generated DAG
project/
├── app.py
├── SETTINGS.yaml
├── task_dag/
│ ├── contracts.py
│ ├── handlers.py
│ └── runtime.py
└── tests/
When a plan is runtime data, use TaskDAG / Dynamic Task. Validate and resolve
the DAG through the TaskDAG path, then let TaskDAGExecutor.async_run(...) use
the TriggerFlow substrate. Do not compile unvalidated submitted or
model-generated plan data directly into new TriggerFlow definitions. Blocks is
an explicit opt-in only when Blocks lifecycle evidence or
ExecutionBlockGraph output is required.
Add delivery adapters only when needed
An application that exposes both HTTP and MCP can add:
services/
├── contracts.py # shared approved public projection, when actually shared
├── api.py # direct FastAPI inbound adapter
└── mcp_server.py # direct FastMCP server adapter
Both transports should validate admission, issue host-owned task identity, call the same async application entry point, and return the same approved public projection. They must not become workflow-policy owners.
from uuid import uuid4
# services/api.py
@app.post("/analysis", response_model=AnalysisResponse)
async def analyze(request: AnalysisRequest) -> AnalysisResponse:
task_id = f"analysis-{uuid4().hex}"
run = await run_analysis(
request.question,
task_id=task_id,
max_concurrency=request.max_concurrency,
)
return project_analysis_run(task_id, run)
# services/mcp_server.py
@mcp.tool
async def analyze_with_mcp(
question: str,
max_concurrency: int = 4,
) -> dict[str, object]:
request = AnalysisRequest(
question=question,
max_concurrency=max_concurrency,
)
task_id = f"analysis-{uuid4().hex}"
run = await run_analysis(
request.question,
task_id=task_id,
max_concurrency=request.max_concurrency,
)
return project_analysis_run(task_id, run).model_dump()
The runnable template contains complete imports, settings lifespans, task-local paths, and in-process transport tests. The abbreviated example above shows only the ownership relationship.
FastAPIHelper remains available when its packaged task/stream protocol is the
public contract you want. It is not deprecated, but direct FastAPI is the
default for an ordinary typed HTTP route. MCP client consumption already
belongs to Agently Action management; do not add another local MCP-client
service or a forwarding-only registration wrapper.
Keep Prompt and output contracts explicit
Keep stable Prompt contracts in YAML or JSON when they evolve independently:
input: current runtime facts;info: authoritative facts, API/schema documentation, signatures, docstrings, evidence, and offered key sets;instruct: transformation and call rules;output: the exact machine-consumable result.
Describe every downstream-consumed field with type, meaning, requiredness, enum or format, range, nullability, and cross-field constraints. Host code must still validate the result before an external call or side effect.
When the model selects a host record, give it one trusted selection key and only task-relevant facts. Validate that key against the offered set and rebuild canonical ids and metadata in host code. Do not make the model copy UUIDs, multiple ids, URLs, or unrelated metadata.
Do not request or store hidden chain-of-thought. A bounded task-specific process
field is acceptable only when its semantic role, evidence boundary, type and
bounds, consumer, visibility, retention, failure behavior, and quality-evidence
status are explicit. A generic unconsumed reasoning, analysis, or thinking
field is not a quality mechanism.
Keep information easy to find
Minimize the cross-file lookup count and nesting depth required by people and coding agents to understand the current behavior. Do not split one-use schemas, constants, helpers, classes, or wrappers into separate locations unless the new boundary has actual reuse value or an independently owned/versioned contract. Formal separation without either benefit is over-design.
Remove unowned wrappers
Before adding a Service, Manager, Factory, request wrapper, repository facade, or adapter, require at least one real owner:
- authorization, validation, policy, or safety;
- lifecycle, state, cleanup, retry, concurrency, or transaction scope;
- a stable external contract or non-trivial representation translation;
- multiple consumers of the exact same contract;
- an already released compatibility boundary.
Otherwise inline it. Remove renaming-only functions, forwarding-only managers, empty packages, unused output nodes, and duplicate facades. Concision is an ownership and consumer property, not a universal line-count limit.
Result, state, and evidence boundaries
- Await
async_get_data()directly when nobody consumes progressive output. Never drain aninstantgenerator into a no-op loop. - Treat consumed
instantfields as provisional; irreversible effects wait for the final parsed and validated result. - Keep per-run data in TriggerFlow execution state.
flow_dataremains shared on the flow object even though save/load serializes and replaces its value. - Use an explicit execution handle for observation, external emit, pause/resume, save/load, intervention, cancellation, or host-controlled close.
- Let trace record bounded facts and Eval judge semantic quality. Do not repeat full prompts, deltas, secrets, or raw metadata in every event.
Test deterministic contracts first: settings, Prompt/output schema, host validation, TriggerFlow state and joins, TaskDAG admission, service projection, and trace allowlists. Mocks prove wiring, not model semantics; executable Prompt or semantic-behavior changes need explicit criteria and the smallest authorized representative real-model check.