Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 27 additions & 12 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ agentic-api/
crates/
agentic-server-core/ # "agentic_core" — pure Rust orchestration library
agentic-server/ # axum HTTP/WS gateway + the `agentic` CLI launcher
agentic-llm-d/ # split-execution state backend for the llm-d coordinator
agentic-praxis/ # placeholder: future Praxis gateway adapter
```

Expand All @@ -30,13 +31,19 @@ agentic-api/
calls into `agentic_core`, and streams the result back. It also happens to host a
second, unrelated binary — a CLI launcher (`agentic`) that spawns the gateway and a
coding harness (Codex/Claude Code) as subprocesses for local use.
- **`agentic-llm-d`** is a separate axum backend for the llm-d coordinator, which runs
inference itself. Its [router](crates/agentic-llm-d/src/lib.rs) exposes
`/v1alpha/responses/hydrate` and `/v1alpha/responses/persist`, plus health/readiness
probes. It uses `agentic_core` for state services and does not proxy or call a model.
- **`agentic-praxis`** is currently a placeholder. Per ADR-03, the intent is for it to
wrap each `agentic-server-core` public function as an `HttpFilter` so Praxis can
compose the agentic loop declaratively instead of going through `agentic-server`'s
axum router. Nothing is implemented there yet.

The dependency direction is one-way: `agentic-server` depends on `agentic-server-core`,
never the reverse. `agentic-server-core` has no knowledge of axum, HTTP, or WebSockets.
The dependency direction is one-way: `agentic-server` and `agentic-llm-d` depend on
`agentic-server-core`, never the reverse. `agentic-server-core` has no axum dependency;
client-facing HTTP/WS transport belongs to the adapter crates, while upstream HTTP/SSE
I/O lives in core inference transport.

## Request flow at a glance

Expand Down Expand Up @@ -496,21 +503,29 @@ As noted above, the round-by-round loop itself is `engine.rs::run_gateway_tool_l
- `GatewayScheduler::plan` creates one slot per gateway-owned function call. Each slot
owns the original item index, public output index, typed `GatewayBinding`, and
lifecycle projection; a missing executor is represented by an explicit slot rather
than omitted from a parallel vector. `GatewayScheduler::execute` then returns one
ordered `GatewayCallResult` per slot. A `futures::stream::buffered` sliding window
bounds fan-out using `tools.max_concurrent_gateway_calls` (default `5`, configurable
through `AGENTIC_MAX_CONCURRENT_GATEWAY_CALLS`). The setting is a nonzero value
carried by the owning `ExecutionContext` into each scheduler, so independent
contexts do not share process-global policy and `.buffered(0)` is unrepresentable.
Completion may occur out of order, but the collected result order always matches
model call order.
than omitted from a parallel vector. `GatewayScheduler::execute_with_budget` then returns one
ordered `GatewayCallResult` per slot. `futures::future::join_all` polls all planned
calls, while a `tokio::sync::Semaphore` created by each `execute_with_budget` invocation limits
active tool executions in that round using `tools.max_concurrent_gateway_calls`
(default `5`, configurable through `AGENTIC_MAX_CONCURRENT_GATEWAY_CALLS`). This
nonzero setting is carried by the owning `ExecutionContext` into each scheduler;
the permits are local to the round, not a process-wide or cross-request limit.
Completion may occur out of order, but `join_all` preserves model call order in
the collected results. The permit limit bounds execution, not the number of
planned call futures waiting for permits.
- A normalized `web_search` function call may batch at most five queries. The JSON
Schema advertises the ceiling and the handler enforces it again because normalized
web search currently uses non-strict arguments. Provider searches acquire a shared
handler semaphore initialized from `tools.max_concurrent_gateway_calls`, preventing
batched calls from multiplying the configured outbound concurrency. Results remain
collected in query order for the public `web_search_call.action.queries` projection.
- Every call has an independent 60-second timeout. Timeout, execution, and tool-config
- Each bound call first acquires its optional same-tool exclusion permit, then a
round execution permit, then a materialization permit shared by cloned scheduler
policies (also used by MCP discovery, with a limit of 16). Waiting for same-tool
exclusion does not consume a round permit; waiting for materialization does. The
independent 60-second timeout wraps `GatewayBinding::execute` only after all
permits are acquired. These permit waits do not count toward it, so it is not a
deadline for the entire round or total call latency. Timeout, execution, and tool-config
failures become failed tool outputs that can be fed back to the model instead of
failing the whole response. A tool registered as gateway-owned without an
implementation (currently file search/code interpreter) likewise produces an error
Expand Down Expand Up @@ -548,7 +563,7 @@ The round decision remains in `engine.rs`, after gateway execution:
`parallel_tool_calls` is an upstream model-generation preference, not a gateway
scheduler switch. It is forwarded to vLLM for all supported declaration mixtures and
defaults to `false` when omitted. Whatever calls the model emits are executed under
the global sliding window and each handler's same-tool safety policy.
the per-round execution permit limit and each handler's same-tool safety policy.

#### `messages_loop.rs` / `messages_request.rs` / `messages_stream.rs`

Expand Down
9 changes: 5 additions & 4 deletions crates/agentic-server-core/src/executor/accumulator.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
//! Response accumulation and parsing utilities.
//!
//! Handles both streaming (SSE) and non-streaming JSON response formats,
//! accumulating chunks into a unified `ResponsePayload` structure.
//! accumulating semantic events into a unified `ResponsePayload` structure.
//!
//! Streaming path uses a channel + `spawn_blocking` so that SSE JSON parsing
//! runs on a blocking thread while the async task continues reading from the
//! network — keeping the tokio executor thread free between chunk arrivals.
//! The executor path in `upstream.rs` processes SSE lines inline using this
//! accumulator. The separate [`ResponseAccumulator::from_stream`] convenience
//! method uses a channel and `spawn_blocking` worker; that worker is not the
//! executor's main streaming path.

use std::collections::{HashMap, HashSet};
use std::pin::Pin;
Expand Down