From a39ccd76ef604ed2c07e8a464ce05268552545c7 Mon Sep 17 00:00:00 2001 From: Mahmoud Mabrouk Date: Fri, 19 Jun 2026 17:39:06 +0200 Subject: [PATCH] docs(agent): agent-workflows ground truth + WP-label comment hygiene Make the agent-workflows design folder readable before reviewers look at behavior, and strip stale work-package labels from live comments. - Add current-state pages: ground-truth, meeting-alignment, implementation-review, pr-stack, agent-template, protocol, triggers, status. - Move historical scratch/ POC material under trash/ (kept for provenance). - Remove WP-2/WP-6/WP-8 and "legacy"/old-name references from code comments in app.py, schemas.py, harnesses.py, docker-compose.dev.yml, and the runner README. No behavior change. --- docs/design/agent-workflows/README.md | 117 +++--- .../agent-workflows/adapters/claude-code.md | 2 +- docs/design/agent-workflows/adapters/pi.md | 2 +- docs/design/agent-workflows/agent-template.md | 59 +++ docs/design/agent-workflows/architecture.md | 299 +++++++--------- docs/design/agent-workflows/ground-truth.md | 85 +++++ .../agent-workflows/implementation-review.md | 159 +++++++++ .../agent-workflows/meeting-alignment.md | 142 ++++++++ .../agent-workflows/ports-and-adapters.md | 335 ++++++++---------- docs/design/agent-workflows/pr-stack.md | 223 ++++++++++++ docs/design/agent-workflows/protocol.md | 141 ++++++++ .../agent-workflows/sdk-local-tools/README.md | 4 +- .../sdk-local-tools/context.md | 2 +- .../sdk-local-tools/organization-proposal.md | 6 +- .../agent-workflows/sdk-local-tools/plan.md | 4 +- .../sdk-local-tools/research.md | 7 +- .../agent-workflows/sdk-local-tools/status.md | 2 +- docs/design/agent-workflows/sessions.md | 199 ++++++----- docs/design/agent-workflows/status.md | 70 ++++ docs/design/agent-workflows/trash/README.md | 16 + .../harness-port-redesign/README.md | 0 .../harness-port-redesign/implementation.md | 0 .../harness-port-redesign/plan.md | 0 .../harness-port-redesign/proposal.md | 0 .../harness-port-redesign/research.md | 0 .../harness-port-redesign/status.md | 0 .../old-rfcs}/agent-protocol-rfc.md | 0 .../old-rfcs}/streaming-and-sessions.md | 0 .../research/auth-secrets.md | 0 .../research/daytona-sandbox.md | 0 .../research/diskless-in-memory-config.md | 0 .../research/open-questions.md | 0 .../research/otel-instrumentation.md | 0 .../research/pi-interaction.md | 0 .../research/sandbox-sharing.md | 0 .../sdk-local-backend/status.md | 0 .../wp-1-pi-tracing/README.md | 0 .../integrating-the-tracing-extension.md | 0 .../wp-1-pi-tracing/poc/.env.example | 0 .../wp-1-pi-tracing/poc/README.md | 0 .../wp-1-pi-tracing/poc/agenta-otel.ts | 0 .../wp-1-pi-tracing/poc/package.json | 0 .../wp-1-pi-tracing/poc/pnpm-lock.yaml | 0 .../wp-1-pi-tracing/poc/run.ts | 0 .../tracing-in-the-agent-service.md | 0 .../wp-2-agent-service/README.md | 0 .../wp-2-agent-service/implementation-plan.md | 0 .../wp-2-agent-service/qa.md | 0 .../wp-3-daytona-sandbox/README.md | 0 .../wp-3-daytona-sandbox/poc/README.md | 0 .../poc/bench_coldstart.py | 0 .../poc/build_snapshot.py | 0 .../wp-3-daytona-sandbox/poc/cleanup.py | 0 .../wp-3-daytona-sandbox/poc/run_agent.py | 0 .../wp-4-multi-message-output/README.md | 0 .../wp-5-chat-vs-completion/README.md | 0 .../wp-6-workflow-type-and-template/README.md | 0 .../{scratch => trash}/wp-7-tools/README.md | 0 .../wp-8-rivet-acp-runtime/README.md | 0 .../wp-8-rivet-acp-runtime/architecture.md | 0 .../wp-8-rivet-acp-runtime/context.md | 0 .../isolation-and-fork.md | 0 .../wp-8-rivet-acp-runtime/plan.md | 0 .../poc/build_rivet_snapshot.py | 16 +- .../poc/commit_agent_config.py | 0 .../poc/debug-events.ts | 0 .../wp-8-rivet-acp-runtime/poc/dump-full.ts | 0 .../wp-8-rivet-acp-runtime/poc/package.json | 0 .../wp-8-rivet-acp-runtime/poc/spike.ts | 0 .../wp-8-rivet-acp-runtime/research.md | 0 .../wp-8-rivet-acp-runtime/status.md | 0 docs/design/agent-workflows/triggers.md | 69 ++++ docs/design/vault-named-secrets/README.md | 25 ++ docs/design/vault-named-secrets/context.md | 41 +++ docs/design/vault-named-secrets/plan.md | 164 +++++++++ docs/design/vault-named-secrets/research.md | 91 +++++ docs/design/vault-named-secrets/status.md | 48 +++ .../docker-compose/ee/docker-compose.dev.yml | 11 +- .../agenta/sdk/agents/adapters/harnesses.py | 2 +- services/agent/README.md | 21 +- services/oss/src/agent/app.py | 8 +- services/oss/src/agent/schemas.py | 5 +- 82 files changed, 1845 insertions(+), 530 deletions(-) create mode 100644 docs/design/agent-workflows/agent-template.md create mode 100644 docs/design/agent-workflows/ground-truth.md create mode 100644 docs/design/agent-workflows/implementation-review.md create mode 100644 docs/design/agent-workflows/meeting-alignment.md create mode 100644 docs/design/agent-workflows/pr-stack.md create mode 100644 docs/design/agent-workflows/protocol.md create mode 100644 docs/design/agent-workflows/status.md create mode 100644 docs/design/agent-workflows/trash/README.md rename docs/design/agent-workflows/{scratch => trash}/harness-port-redesign/README.md (100%) rename docs/design/agent-workflows/{scratch => trash}/harness-port-redesign/implementation.md (100%) rename docs/design/agent-workflows/{scratch => trash}/harness-port-redesign/plan.md (100%) rename docs/design/agent-workflows/{scratch => trash}/harness-port-redesign/proposal.md (100%) rename docs/design/agent-workflows/{scratch => trash}/harness-port-redesign/research.md (100%) rename docs/design/agent-workflows/{scratch => trash}/harness-port-redesign/status.md (100%) rename docs/design/agent-workflows/{ => trash/old-rfcs}/agent-protocol-rfc.md (100%) rename docs/design/agent-workflows/{ => trash/old-rfcs}/streaming-and-sessions.md (100%) rename docs/design/agent-workflows/{scratch => trash}/research/auth-secrets.md (100%) rename docs/design/agent-workflows/{scratch => trash}/research/daytona-sandbox.md (100%) rename docs/design/agent-workflows/{scratch => trash}/research/diskless-in-memory-config.md (100%) rename docs/design/agent-workflows/{scratch => trash}/research/open-questions.md (100%) rename docs/design/agent-workflows/{scratch => trash}/research/otel-instrumentation.md (100%) rename docs/design/agent-workflows/{scratch => trash}/research/pi-interaction.md (100%) rename docs/design/agent-workflows/{scratch => trash}/research/sandbox-sharing.md (100%) rename docs/design/agent-workflows/{scratch => trash}/sdk-local-backend/status.md (100%) rename docs/design/agent-workflows/{scratch => trash}/wp-1-pi-tracing/README.md (100%) rename docs/design/agent-workflows/{scratch => trash}/wp-1-pi-tracing/integrating-the-tracing-extension.md (100%) rename docs/design/agent-workflows/{scratch => trash}/wp-1-pi-tracing/poc/.env.example (100%) rename docs/design/agent-workflows/{scratch => trash}/wp-1-pi-tracing/poc/README.md (100%) rename docs/design/agent-workflows/{scratch => trash}/wp-1-pi-tracing/poc/agenta-otel.ts (100%) rename docs/design/agent-workflows/{scratch => trash}/wp-1-pi-tracing/poc/package.json (100%) rename docs/design/agent-workflows/{scratch => trash}/wp-1-pi-tracing/poc/pnpm-lock.yaml (100%) rename docs/design/agent-workflows/{scratch => trash}/wp-1-pi-tracing/poc/run.ts (100%) rename docs/design/agent-workflows/{scratch => trash}/wp-1-pi-tracing/tracing-in-the-agent-service.md (100%) rename docs/design/agent-workflows/{scratch => trash}/wp-2-agent-service/README.md (100%) rename docs/design/agent-workflows/{scratch => trash}/wp-2-agent-service/implementation-plan.md (100%) rename docs/design/agent-workflows/{scratch => trash}/wp-2-agent-service/qa.md (100%) rename docs/design/agent-workflows/{scratch => trash}/wp-3-daytona-sandbox/README.md (100%) rename docs/design/agent-workflows/{scratch => trash}/wp-3-daytona-sandbox/poc/README.md (100%) rename docs/design/agent-workflows/{scratch => trash}/wp-3-daytona-sandbox/poc/bench_coldstart.py (100%) rename docs/design/agent-workflows/{scratch => trash}/wp-3-daytona-sandbox/poc/build_snapshot.py (100%) rename docs/design/agent-workflows/{scratch => trash}/wp-3-daytona-sandbox/poc/cleanup.py (100%) rename docs/design/agent-workflows/{scratch => trash}/wp-3-daytona-sandbox/poc/run_agent.py (100%) rename docs/design/agent-workflows/{scratch => trash}/wp-4-multi-message-output/README.md (100%) rename docs/design/agent-workflows/{scratch => trash}/wp-5-chat-vs-completion/README.md (100%) rename docs/design/agent-workflows/{scratch => trash}/wp-6-workflow-type-and-template/README.md (100%) rename docs/design/agent-workflows/{scratch => trash}/wp-7-tools/README.md (100%) rename docs/design/agent-workflows/{scratch => trash}/wp-8-rivet-acp-runtime/README.md (100%) rename docs/design/agent-workflows/{scratch => trash}/wp-8-rivet-acp-runtime/architecture.md (100%) rename docs/design/agent-workflows/{scratch => trash}/wp-8-rivet-acp-runtime/context.md (100%) rename docs/design/agent-workflows/{scratch => trash}/wp-8-rivet-acp-runtime/isolation-and-fork.md (100%) rename docs/design/agent-workflows/{scratch => trash}/wp-8-rivet-acp-runtime/plan.md (100%) rename docs/design/agent-workflows/{scratch => trash}/wp-8-rivet-acp-runtime/poc/build_rivet_snapshot.py (69%) rename docs/design/agent-workflows/{scratch => trash}/wp-8-rivet-acp-runtime/poc/commit_agent_config.py (100%) rename docs/design/agent-workflows/{scratch => trash}/wp-8-rivet-acp-runtime/poc/debug-events.ts (100%) rename docs/design/agent-workflows/{scratch => trash}/wp-8-rivet-acp-runtime/poc/dump-full.ts (100%) rename docs/design/agent-workflows/{scratch => trash}/wp-8-rivet-acp-runtime/poc/package.json (100%) rename docs/design/agent-workflows/{scratch => trash}/wp-8-rivet-acp-runtime/poc/spike.ts (100%) rename docs/design/agent-workflows/{scratch => trash}/wp-8-rivet-acp-runtime/research.md (100%) rename docs/design/agent-workflows/{scratch => trash}/wp-8-rivet-acp-runtime/status.md (100%) create mode 100644 docs/design/agent-workflows/triggers.md create mode 100644 docs/design/vault-named-secrets/README.md create mode 100644 docs/design/vault-named-secrets/context.md create mode 100644 docs/design/vault-named-secrets/plan.md create mode 100644 docs/design/vault-named-secrets/research.md create mode 100644 docs/design/vault-named-secrets/status.md diff --git a/docs/design/agent-workflows/README.md b/docs/design/agent-workflows/README.md index 90a12c2b2c..6eb60c47cf 100644 --- a/docs/design/agent-workflows/README.md +++ b/docs/design/agent-workflows/README.md @@ -1,53 +1,64 @@ -# Agent workflows - -This folder documents a proof of concept: running a coding agent as an Agenta workflow. - -Agenta runs prompt workflows today (completion, chat, the LLM judge). Each calls a model -once and returns one answer. An agent is different. It runs a loop, calls tools across many -turns, and returns a final answer. This PoC adds the agent as a new workflow type behind the -same `/invoke` contract, traced into the same spans, configured from the same playground. - -It proves one specific claim: that the **agent** and the **place it runs** are both config, -not code. You change a dropdown to swap Pi for Claude Code, or local for a Daytona cloud -sandbox, and nothing above the seam changes. - -## Read in this order - -1. **[Architecture](architecture.md)**. How a request flows from the playground to the model - and back: the relay of programs, the two containers, and the vocabulary. Start here. -2. **[Ports and adapters](ports-and-adapters.md)**. The ports that keep the relay swappable: - the backend, environment, and harness layers, where they live in the SDK, the wire - contract, and how the service picks a backend. -3. **[Sessions](sessions.md)**. How a multi-turn conversation holds together today (cold - replay), and the two paths open to us tomorrow. -4. **[The Pi adapter](adapters/pi.md)**. The default harness, which traces itself and takes - tools natively through a Pi extension. -5. **[The Claude Code adapter](adapters/claude-code.md)**. The second harness, which proves - the swap and is the template for any MCP-capable agent. -6. **[The Agenta harness](adapters/agenta.md)**. Pi with an opinion: forced skills, forced - tools, and a base AGENTS.md preamble the author's instructions are appended to. - -## What this PoC includes and defers - -It includes the agent workflow behind `/invoke`, two harnesses (Pi and Claude Code), two -sandboxes (local and Daytona), backend-resolved tools that keep credentials server-side, and -tracing that nests the agent's run under the caller's span. - -It defers the things a production rollout will need: a warm daemon and server-owned session -storage (see [Sessions](sessions.md)), live streaming to the client over the HTTP edge, the -multi-tenant filesystem jail for a shared daemon, and registering the agent as a first-class -backend workflow type with its own builtin URI. Each is called out where it belongs. - -The first two of those, streaming and server-owned sessions, have a proposed design: -[Streaming and sessions](streaming-and-sessions.md) for the rationale and trade-offs, and -the [Agent protocol RFC](agent-protocol-rfc.md) for the normative spec of the endpoints and -the wire format. They add a new `POST /messages` endpoint (Vercel-AI-format SSE stream, an -optional `session_id`, and `UIMessage` inputs) plus a `load-session` endpoint, sitting next -to the existing `/invoke`, which is unchanged. - -## The `scratch/` folder - -`scratch/` holds the raw working material from the build: the original work-package folders -(WP-1 through WP-8), the port redesign notes, the research write-ups, and the proof-of-concept -spikes. The pages above supersede it. It stays for history and for the running POC code, and -it is not meant to be read as the design. +# Agent Workflows + +This workspace documents the current agent workflow implementation and the work still +needed to make it production-ready. + +The source of truth is the code listed in [Ground Truth](ground-truth.md). Design pages at +this level describe the current implementation unless they explicitly say "planned", +"blocked", or "not implemented". Historical work-package notes and old RFCs live in +[trash/](trash/). + +## Read In This Order + +1. [Ground Truth](ground-truth.md): what the current code does, what is wired, and what is + still missing. +2. [Status](status.md): current cleanup state, decisions, blockers, and next steps. +3. [Meeting Alignment](meeting-alignment.md): where the current work matches the June 18 + design discussion, where it diverges, and what still needs to be done. +4. [Architecture](architecture.md): the service, agent runner sidecar, harnesses, and + sandboxes. +5. [Protocol](protocol.md): `/invoke`, `/messages`, `/load-session`, and the runner `/run` + wire contract. +6. [Ports and Adapters](ports-and-adapters.md): the SDK runtime ports, backend adapters, + harness adapters, and browser protocol adapter. +7. [Agent Template](agent-template.md): the intended split between generic agent identity, + harness-specific config, and runtime infrastructure. +8. [Sessions](sessions.md): cold replay, streaming, session ids, and the missing session + store. +9. [Triggers](triggers.md): planned trigger/event integration and the missing Compose.io + POC. +10. [Pi Adapter](adapters/pi.md): Pi-specific tool delivery, prompt layers, tracing, and + usage writeback. +11. [Claude Code Adapter](adapters/claude-code.md): Claude over ACP, MCP tool delivery, + permissions, tracing, and usage. +12. [Agenta Harness](adapters/agenta.md): the experimental Agenta-flavored Pi harness. +13. [SDK Local Tools](sdk-local-tools/): planned and partly implemented work for standalone + SDK tool resolution. This remains blocked by `LocalBackend`. +14. [PR Stack](pr-stack.md): functional breakpoints for reviewable stacked PRs. +15. [Implementation Review](implementation-review.md): high-level cleanup risks and PR + slicing notes. +16. [Open Issues](open-issues.md): deferred decisions that need ownership. + +## Current State + +The agent workflow runs a coding harness as an Agenta workflow. It supports: + +- A batch `/invoke` path that returns the final assistant message. +- An agent-only `/messages` path that accepts Vercel `UIMessage` input and can stream a + Vercel UI Message Stream over SSE. +- A `/load-session` route with the right contract but no durable storage by default. +- Pi and Claude harnesses through the rivet runner. +- Pi and the experimental `agenta` harness through the in-process Pi backend. +- Server-resolved tool specs, code tool execution, callback tools, and MCP plumbing behind + a feature flag. + +The main missing pieces are durable server-owned sessions, future session snapshot +interfaces, the agent template/config split, trigger integration, a working standalone +`LocalBackend`, production Agenta harness content, first-class built-in workflow +registration, and the final cleanup of historical work-package names in comments and docs. + +## Trash + +[trash/](trash/) holds old work-package notes, research spikes, and superseded RFCs. It is +kept for archaeology only. Do not treat it as design truth unless a current page links to a +specific note as background. diff --git a/docs/design/agent-workflows/adapters/claude-code.md b/docs/design/agent-workflows/adapters/claude-code.md index 64a4e3c96c..cc2e127f39 100644 --- a/docs/design/agent-workflows/adapters/claude-code.md +++ b/docs/design/agent-workflows/adapters/claude-code.md @@ -31,7 +31,7 @@ something in particular. The mechanism is a small stdio MCP server (`tools/mcp-server.ts`) that the daemon launches and attaches to the session. Its tool bodies POST back to Agenta's `/tools/call` with the -same WP-7 envelope the Pi path uses. The resolved specs and the callback endpoint reach the +same callback-tool envelope the Pi path uses. The resolved specs and the callback endpoint reach the MCP server through its environment, so nothing tool-specific is written to a file the agent can read. The safety property is identical to Pi's: the provider key and the connection auth stay server-side, and the agent only ever asks Agenta to run a named tool. diff --git a/docs/design/agent-workflows/adapters/pi.md b/docs/design/agent-workflows/adapters/pi.md index abcd9ced87..e6c32154bf 100644 --- a/docs/design/agent-workflows/adapters/pi.md +++ b/docs/design/agent-workflows/adapters/pi.md @@ -38,7 +38,7 @@ extension reads the resolved tool specs from `AGENTA_TOOL_SPECS` and registers e Pi directly through `pi.registerTool`. Pi then sees them as native tools and runs the loop. Each registered tool's body does one thing: it POSTs the call back to Agenta's `/tools/call` -with the tool's `callRef` (the WP-7 envelope). The model picks the tool and supplies the +with the tool's `callRef` (the callback-tool envelope). The model picks the tool and supplies the arguments; Agenta runs the actual tool server-side. This is the key safety property: the Composio key and the connection auth never enter the sandbox. The agent only ever asks Agenta to run a named tool. diff --git a/docs/design/agent-workflows/agent-template.md b/docs/design/agent-workflows/agent-template.md new file mode 100644 index 0000000000..e62b416606 --- /dev/null +++ b/docs/design/agent-workflows/agent-template.md @@ -0,0 +1,59 @@ +# Agent Template + +The agent template is the portable description of what the agent is. It should not silently +mix product identity, harness runtime options, and deployment infrastructure. + +## Intended Shape + +The baseline template is: + +- `AGENTS.md` content: the main instructions. +- Skills: a folder-shaped set of skill files, serialized into a JSON-safe representation. +- Tools: managed builtin tools, inline code tools, and future MCP tool references. +- Metadata: name, description, and other product identity fields when the UI needs them. + +This mirrors the file-based agent convention used by local coding agents while keeping the +wire shape JSON-friendly. File bytes can be base64 encoded when plain text is not safe. + +## Configuration Layers + +| Layer | Examples | Status | +| --- | --- | --- | +| Generic agent identity | `AGENTS.md`, skills, tool references, template metadata | Intended long-term template surface. Partly represented today by `agents_md` and tool config. | +| Harness-specific config | Harness id, model, harness option bags, permission policy | Present today. Permissions are not generic yet. | +| Runtime infrastructure | Local versus Daytona, runner sidecar URL, filesystem isolation, secret channels | Present as a POC selection in `RunSelection`, but should not become durable agent identity by default. | + +The current code still accepts `sandbox` in request config. That is useful for the POC and +tests, but the long-term template should not require users to encode where the platform is +deployed. + +## Current Implementation + +Today the request surface includes: + +- `parameters.agent.agents_md` +- `parameters.agent.model` +- `parameters.agent.tools` +- `parameters.agent.mcp_servers` +- `parameters.agent.harness` +- `parameters.agent.sandbox` +- `parameters.agent.permission_policy` +- harness-specific options such as Pi prompt overrides + +The runtime also has forced Agenta policy in `AgentaHarness`, but that content is +experimental and not a general template system. + +## Missing Work + +- A first-class persisted template DTO that separates identity from run selection. +- Skills folder serialization and loading outside forced Agenta harness content. +- A stable tool contract based on URI, schema, and execution body or delivery reference. +- Clear UI grouping for generic template fields, harness-specific fields, and runtime + infrastructure. +- Import/export behavior for `AGENTS.md` and skills folders. + +## Deferred Work + +Hooks, assets, extra code snippets, and a generic permissions overlay are deferred. The POC +should leave space for them without pretending they are supported. + diff --git a/docs/design/agent-workflows/architecture.md b/docs/design/agent-workflows/architecture.md index 069a4ba7fa..8304d73d9f 100644 --- a/docs/design/agent-workflows/architecture.md +++ b/docs/design/agent-workflows/architecture.md @@ -1,183 +1,142 @@ # Architecture -This page explains how an agent runs inside Agenta, from the moment a request arrives -to the moment the answer comes back. Read it first. The other pages go deeper into the -[ports and adapters](ports-and-adapters.md), [sessions](sessions.md), and the two -shipped adapters ([Pi](adapters/pi.md), [Claude Code](adapters/claude-code.md)). +This page explains how the current agent workflow runs. It describes the checked-in code, +not the older work-package plans in [trash/](trash/). -## What an agent workflow is +## The Model -Agenta already runs prompt workflows: completion, chat, and the LLM judge. Each one calls -a model once and returns one answer. An agent is different. It runs a loop. It reads its -instructions, calls a model, runs a tool, reads the result, and calls the model again. It -keeps going until the task is done, then returns the final answer. +Agenta already runs prompt workflows that call a model once and return one answer. An +agent workflow runs a coding harness instead. The harness reads instructions, calls a +model, calls tools, observes the results, and loops until it has an answer. -This PoC adds the agent as a new kind of workflow. It sits behind the same `/invoke` -endpoint every other workflow uses, traces into the same spans, and reads its config from -the same playground. +The implementation keeps two choices configurable: -The loop itself is not the hard part. Open-source coding agents already run the loop well. -The hard part is running one of those agents *as an Agenta workflow*: behind the standard -contract, traced into the standard spans, with the agent and the place it runs both -swappable by config. That is the problem this architecture solves. +- **Harness:** which agent runs. Current values are `pi`, `claude`, and experimental + `agenta`. +- **Sandbox:** where the run happens. Current values are `local` and `daytona` on the + rivet path. The in-process Pi path is local only. -## The core idea: a relay of programs +The platform still exposes the agent through normal workflow routing. `/invoke` remains the +batch contract. Agent routes also register `/messages` and `/load-session` for the browser +chat protocol. -The system is a relay. Each program starts the next one and passes work down the line. The -prompt travels down the relay, and the answer travels back up. +## Runtime Shape -Here is the whole relay for a normal local run: +The deployed local stack uses two containers. ``` - browser / playground - │ POST /invoke - ▼ - ┌─────────────────────────────────────────────────── - │ CONTAINER 1: "services" (Python / FastAPI) - │ the Agenta backend. Parses the request, - │ gathers config, and calls the runner. - └─────────────────────────────────────────────────── - │ POST http://agent-pi:8765/run - ▼ - ┌─────────────────────────────────────────────────── - │ CONTAINER 2: "agent-pi" (Node / TypeScript) - │ the sidecar. server.ts → engines/rivet.ts - │ - │ rivet daemon (subprocess) - │ └── ACP adapter: pi-acp (subprocess) - │ └── pi (subprocess) ← the harness - └─────────────────────────────────────────────────── - │ HTTPS - ▼ - OpenAI / Anthropic (the model) +browser / playground + | + | POST /invoke or POST /messages + v +services container + Python workflow handler + services/oss/src/agent/app.py + | + | POST /run, or spawn the runner CLI in local checkout mode + v +agent runner sidecar + compose service: agent-pi + Node HTTP server + services/agent/src/server.ts + | + +-- in-process Pi engine + | services/agent/src/engines/pi.ts + | + +-- rivet engine + services/agent/src/engines/rivet.ts + | + +-- sandbox-agent daemon + | + +-- ACP adapter: pi-acp or claude-agent-acp + | + +-- harness CLI: pi or claude ``` -Two containers carry the request. Inside the second one, a small tree of processes does -the work. Each box has a clear job, and the next sections name them. - -## The two containers - -The deployment runs two containers that matter here. Both stay up all the time. You can -see both in `hosting/docker-compose/ee/docker-compose.dev.yml`. - -The **`services`** container runs the Python backend. Every Agenta workflow lives here, -including the agent. When you run an agent in the playground, the request lands in this -container. The handler reads the config (which agent, which model, the instructions, the -tools, the provider keys), builds one request, and calls the runner over HTTP. - -The **`agent-pi`** container is the sidecar. It runs a small Node web server on port 8765. -Its only job is to receive a `POST /run`, drive the agent, and return the result. The -`services` container reaches it on the internal network at `http://agent-pi:8765`. - -"Sidecar" just names a small helper container that runs next to a main one. Two reasons -justify the split. The agent code is TypeScript and the backend is Python, so they want -different runtimes. And the sidecar deliberately holds none of the stack's secrets (it has -no `env_file`), so a sandboxed agent cannot read the platform's Stripe or Composio keys. - -## Inside the sidecar: the process tree - -The sidecar does not run the agent itself. When a `/run` request arrives, its TypeScript -starts a chain of child processes, and each one starts the next. - -1. **The rivet daemon** (`sandbox-agent server`). Our code spawns it as a child process. - It is a binary from the open-source [`rivet-dev/sandbox-agent`](https://github.com/rivet-dev/sandbox-agent) - project (Apache-2.0). Think of it as a manager. You tell it "run agent `pi` with this - prompt," and it handles the work of launching the agent and streaming results back. - -2. **The ACP adapter** (`pi-acp`, or `claude-agent-acp` for Claude). The daemon spawns it - as a child process. It is a translator. It speaks ACP on the side facing the daemon and - the agent's own protocol on the side facing the agent. - -3. **The harness** (`pi`, or the `claude` CLI). The adapter spawns it as a child process. - This is the real coding agent. It reads the instructions, calls the model, runs tools, - and loops until the task is done. - -All three run as processes inside the `agent-pi` container. They are not separate -containers. They form a parent-child-grandchild tree. - -## The vocabulary, defined once - -| Term | What it is | -| --- | --- | -| **Harness** | The coding agent program. Pi, Claude Code, and Codex are harnesses. Each is a CLI that takes instructions, calls a model, runs tools, and loops. "Harness" is our umbrella word for "the agent engine." | -| **ACP** (Agent Client Protocol) | A shared language for talking to any coding agent. Without it, each agent has its own API and you write custom glue per agent. With it, you speak one protocol and the agent on the far end is swappable. This is why one config value flips `pi` to `claude`. | -| **ACP adapter** | The translator that makes one specific agent speak ACP. Pi does not speak ACP on its own, so `pi-acp` wraps it. Claude has `claude-agent-acp`. | -| **rivet daemon** | The manager that starts the adapter and harness, hides *where* they run, and streams their events back over ACP. We use it; we did not write it. | -| **Sandbox** | *Where* the agent's process tree runs. `local` means processes inside the sidecar. `daytona` means a throwaway cloud machine. | -| **Sidecar** | The always-on helper container (`agent-pi`) that drives runs. Not the sandbox. The sidecar starts the sandbox. | - -## Two axes you can change: harness and sandbox - -The whole point of the relay is that two pieces swap independently, by config, with no code -change. The playground exposes both as dropdowns. - -- **Harness** chooses *which* agent runs: `pi` or `claude`. It becomes the rivet `agent` - value, which selects the ACP adapter. -- **Sandbox** chooses *where* the agent's process tree runs: `local` or `daytona`. - -The two are orthogonal. You can run `pi` locally, `claude` locally, or `pi` on Daytona, and -each is one dropdown change. The request also carries a **permission policy** (`auto` or -`deny`) that decides how a permission-gating harness like Claude handles tool prompts in a -run with no human watching. - -## Local versus Daytona: the same tree, a different place - -The relay above is `sandbox: local`. The daemon, adapter, and harness all run as processes -inside the `agent-pi` container, on our own server. - -Switch to `sandbox: daytona` and one thing changes. That same tree runs in a Daytona cloud -sandbox instead. Daytona starts a throwaway remote machine, the daemon and adapter and -harness run there, and the sidecar talks to them over HTTP. Everything else is identical. - -So the sidecar is not the sandbox. The sidecar is the always-on driver. The sandbox is the -place the agent runs, which is either "processes inside the sidecar" (`local`) or "a cloud -machine the sidecar talks to" (`daytona`). - -## The lifecycle: cold per run - -Nothing in the process tree stays alive between runs. Only the two containers stay up. -Every invoke starts a fresh daemon, which starts a fresh adapter, which starts a fresh -harness. The run does its work, returns its answer, and then the runner tears the whole -tree down (`destroySandbox` and `dispose` in a `finally` block). The next invoke builds the -tree again from scratch. - -This is the **cold** model. It is simple and well isolated, and it has one consequence -worth stating up front: because no session is held between turns, a multi-turn conversation -replays its history on every turn. [Sessions](sessions.md) covers what that means today and -how a warm model could change it tomorrow. - -## The other engine: in-process Pi - -The relay above describes the **rivet engine**, the default in the deployed stack and the -path the rest of these docs assume. The runner also ships a second engine: **in-process -Pi**. It drives the Pi SDK directly inside the sidecar, with no daemon, adapter, or ACP in -between. It exists for the simplest local case and as a fallback that does not depend on the -rivet daemon. - -The two engines are the two backends behind the same SDK ports: `RivetBackend` and -`InProcessPiBackend`. Both serve the same `/run` contract, so which one runs is a deployment -detail, not a difference the workflow author sees. The -[ports and adapters](ports-and-adapters.md) page explains the ports and the backends. - -## How a request flows, end to end - -Putting it together, a single agent run on `pi` / `local` goes like this: - -1. The playground sends `POST /invoke` to the `services` container. -2. The Python handler (`agent/app.py`) reads the config, resolves the tools and provider - keys, and builds a neutral `AgentConfig` and `SessionConfig` from the SDK runtime - (`agenta.sdk.agents`). -3. It picks a backend (`RivetBackend` here) from the harness and sandbox, wraps it in an - `Environment` and a `Harness`, and the harness sends one `POST /run` over the backend's - transport (HTTP to the sidecar). -4. The sidecar's rivet engine starts the daemon, which starts `pi-acp`, which starts `pi`. -5. `pi` reads the instructions, calls the model, runs any tools, and streams events back up - the relay. Those events become trace spans nested under the `/invoke` span (the - [Pi adapter](adapters/pi.md) page explains who emits them). -6. The harness finishes. The runner reads the final text and the token usage, tears the - tree down, and returns one `/run` result. -7. The Python handler records the usage on the workflow span and returns the assistant - message as the `/invoke` response. - -The next pages explain the seam that makes step 3 engine-agnostic, the session model behind -steps 4 to 6, and exactly how each adapter implements step 5. +The `services` container owns Agenta concerns: workflow routing, config parsing, provider +secret resolution, tool resolution, and trace context. The agent runner sidecar owns the +agent run: it drives Pi directly or drives a harness over ACP through rivet. In Docker +Compose this service is still named `agent-pi`, and the service reaches it through +`AGENTA_AGENT_PI_URL`. + +The sidecar deliberately does not inherit the full stack environment. Provider keys and +tool credentials are resolved by the service and passed only in the scoped run payloads +that need them. + +## Backends + +The SDK runtime models engines as `Backend` adapters. + +| Backend | Status | Harnesses | Sandbox support | Notes | +| --- | --- | --- | --- | --- | +| `InProcessPiBackend` | Implemented | `pi`, `agenta` | `local` only | Drives `services/agent/src/engines/pi.ts`. This is the simple local Pi path. | +| `RivetBackend` | Implemented | `pi`, `claude` | `local`, `daytona` | Drives `services/agent/src/engines/rivet.ts`, which starts `sandbox-agent` and an ACP adapter. | +| `LocalBackend` | Not implemented | Intended: `pi`, `claude` | Local machine | Public class exists, but `create_sandbox` and `create_session` raise `NotImplementedError`. | + +`services/oss/src/agent/app.py` chooses the backend per request. Pi and `agenta` on local +default to `InProcessPiBackend`. Claude, non-local sandboxes, or +`AGENTA_AGENT_RUNTIME=rivet` select `RivetBackend`. + +## Harnesses + +The SDK runtime models agent-specific behavior as `Harness` adapters. + +| Harness | Status | Backend path | Notes | +| --- | --- | --- | --- | +| `PiHarness` | Implemented | In-process Pi or rivet | Native Pi tools, Pi prompt overrides, Pi tracing extension. | +| `ClaudeHarness` | Implemented | Rivet only | MCP tools, permission policy, runner-built tracing. | +| `AgentaHarness` | Experimental | In-process Pi only | Pi with forced tools, forced skill names, and placeholder Agenta prompt layers. | + +`AgentaHarness` with `daytona` or any rivet path is intentionally unsupported today. It +raises through the normal harness/backend compatibility check instead of silently running +without its forced skills. + +## Request Flow + +Batch `/invoke` follows this path: + +1. The workflow route calls `_agent` in `services/oss/src/agent/app.py`. +2. `_agent` parses `AgentConfig` and `RunSelection` from request parameters. +3. The service resolves provider keys, tools, and MCP servers. MCP resolution is gated by + `AGENTA_AGENT_ENABLE_MCP`. +4. The service builds `SessionConfig` and creates a harness over an environment and backend. +5. The harness opens a cold session, sends one `/run` request to the TypeScript runner, and + destroys the session. +6. The service records usage on the workflow span and returns one assistant message. + +Agent `/messages` follows the same runtime path after a browser-protocol adapter step: + +1. `sdks/python/agenta/sdk/agents/adapters/vercel/routing.py` validates or mints + `session_id`. +2. It converts Vercel `UIMessage` parts into neutral agent `Message` objects. +3. It sets `data.stream` from the `Accept` header. +4. `_agent` either returns a batch message or streams an `AgentRun`. +5. The Vercel adapter converts live `AgentEvent` objects into Vercel UI Message Stream + parts and the routing layer frames them as SSE. + +`/load-session` is registered for agent routes, but the default store is +`NoopSessionStore`. It returns an empty message list unless a real `SessionStore` is +injected. + +## Lifecycle + +The runtime is still cold. Each turn creates a fresh session and tears it down after the +turn. Multi-turn context comes from replaying message history, not from a warm daemon or a +persisted model session. + +This cold model keeps isolation simple and makes `/invoke` and `/messages` share the same +runtime. It also means durable server-owned history and warm `session/load` are still future +work. + +## Current Gaps + +- `LocalBackend` is a public adapter shape but does not run anything yet. +- `/load-session` has the route contract but no default persistent store and no write path + from completed turns. +- `AgentaHarness` uses placeholder preamble, persona, and skill content. +- `AgentaHarness` is local in-process only. +- Pi system prompt overrides are not delivered on the rivet ACP path. +- The agent is still registered as a custom workflow handler, not as a first-class builtin + URI such as `agenta:builtin:agent:v0`. +- Historical WP labels remain in several code comments. They should be cleaned in a + documentation and comment hygiene PR. diff --git a/docs/design/agent-workflows/ground-truth.md b/docs/design/agent-workflows/ground-truth.md new file mode 100644 index 0000000000..a62094ff00 --- /dev/null +++ b/docs/design/agent-workflows/ground-truth.md @@ -0,0 +1,85 @@ +# Ground Truth + +This page is the current implementation map. If another design page disagrees with this +page, treat this page and the referenced code as the source of truth. + +## Code Surface + +| Area | Files | Current role | +| --- | --- | --- | +| Agent service handler | `services/oss/src/agent/app.py` | Parses agent config, resolves secrets and tools, chooses a backend, runs batch or streaming turns. | +| Agent route wiring | `sdks/python/agenta/sdk/decorators/routing.py` | Registers `/invoke`, `/inspect`, and agent-only `/messages` plus `/load-session`. | +| Browser protocol adapter | `sdks/python/agenta/sdk/agents/adapters/vercel/` | Converts Vercel `UIMessage` input and emits Vercel UI Message Stream parts. | +| SDK runtime DTOs | `sdks/python/agenta/sdk/agents/dtos.py` | Defines `AgentConfig`, `RunSelection`, `SessionConfig`, messages, events, capabilities, and harness configs. | +| SDK runtime ports | `sdks/python/agenta/sdk/agents/interfaces.py` | Defines `Backend`, `Environment`, `Sandbox`, `Session`, `Harness`, `SessionStore`, and `NoopSessionStore`. | +| Backend adapters | `sdks/python/agenta/sdk/agents/adapters/in_process.py`, `rivet.py`, `local.py` | Implement in-process Pi and rivet backends. `LocalBackend` is a stub. | +| Harness adapters | `sdks/python/agenta/sdk/agents/adapters/harnesses.py` | Maps neutral session config into Pi, Claude, and Agenta harness-specific config. | +| Runner wire | `sdks/python/agenta/sdk/agents/utils/wire.py`, `services/agent/src/protocol.ts` | Keeps the Python and TypeScript `/run` payloads in sync. | +| Runner transports | `sdks/python/agenta/sdk/agents/utils/ts_runner.py`, `services/agent/src/server.ts`, `services/agent/src/cli.ts` | Send one-shot JSON or live NDJSON records to and from the runner. | +| Runner engines | `services/agent/src/engines/pi.ts`, `services/agent/src/engines/rivet.ts` | Run Pi in process or run a harness over ACP through rivet. | +| Tool execution | `sdks/python/agenta/sdk/agents/tools/`, `services/oss/src/agent/tools/`, `services/agent/src/tools/` | Parse tool config, resolve runnable specs, and execute callback, code, and MCP-delivered tools. | +| Tracing | `services/oss/src/agent/tracing.py`, `services/agent/src/tracing/otel.ts`, `services/agent/src/extensions/agenta.ts` | Thread trace context into the run and emit agent spans plus usage. | +| UI config controls | `web/packages/agenta-entity-ui/src/DrillInView/SchemaControls/AgentConfigControl.tsx` | Edits the typed agent config shape in the playground. | + +## Implemented + +- The service exposes an agent workflow handler through `ag.create_app`, `ag.workflow`, and + `ag.route`. +- `/invoke` runs one cold turn and returns the final assistant message. +- Agent routes register `/messages` and `/load-session` when `flags={"is_agent": True}`. +- `/messages` validates or mints `session_id`, folds Vercel `UIMessage` input into neutral + runtime messages, and supports JSON or Vercel SSE based on `Accept`. +- Streaming runs over a runner NDJSON stream internally. The browser edge projects those + events into Vercel UI Message Stream parts and appends `[DONE]`. +- `InProcessPiBackend` supports `pi` and `agenta` on local. +- `RivetBackend` supports `pi` and `claude` on local or Daytona. +- `PiHarness`, `ClaudeHarness`, and `AgentaHarness` exist and validate backend support. +- The tool resolver package exists in the SDK. The service composes SDK tool and MCP + resolvers with service-owned gateway and vault adapters. +- Code tools execute in a subprocess with a minimal allowlisted environment plus scoped + tool secrets. +- Callback tools route through `/tools/call`. On Daytona, Pi tool calls use the runner file + relay. +- MCP delivery exists for non-Pi harnesses through the stdio MCP bridge, but service-side + MCP resolution is feature-gated. + +## Not Implemented + +- `LocalBackend` does not run Pi or Claude. It raises `NotImplementedError`. +- `SessionStore` has no production adapter. The default `NoopSessionStore` returns empty + history and discards writes. +- Completed `/messages` turns are not persisted to a session store by default. +- Harness session snapshots, such as Rivet/ACP state save/load around cleanup/setup, are + not represented by a production port yet. +- Warm daemon sessions, ACP `session/load`, and session fork are not wired. +- `AgentaHarness` does not run on rivet or Daytona. +- `AgentaHarness` ships placeholder Agenta preamble, persona, and skill set. +- The agent is not registered as a first-class built-in workflow type. +- Pi `systemPrompt` and `appendSystemPrompt` are not delivered on the rivet ACP path. +- Remote MCP servers are skipped by the current runner path. Local stdio MCP is the path + represented by the bridge. +- Trigger lifecycle, Compose.io trigger integration, and event-to-agent mapping are not + implemented in the agent workflow code. +- A persisted agent template object that separates `AGENTS.md`, skills, tools, + harness-specific config, and runtime infrastructure does not exist yet. + +## Planned Or Blocked Work + +- [SDK Local Tools](sdk-local-tools/) is a planned and partly implemented workspace for + standalone SDK tool resolution. It remains blocked on `LocalBackend`. +- Durable server-owned sessions need a real `SessionStore`, a write path from completed + turns, ownership checks, and a decision on platform versus local storage. +- Stateful session resume needs research into Rivet/ACP session representation and a + future save/load snapshot interface separate from chat history. +- Trigger integration needs a provider port, a Compose.io adapter, Agenta-owned trigger + state, and event-to-agent mapping. +- The old streaming RFCs are archived in [trash/old-rfcs/](trash/old-rfcs/). They explain + why the protocol exists but no longer describe the exact current state. + +## Verification Pointers + +- `/messages` and `/load-session` routing tests live in + `sdks/python/oss/tests/pytest/utils/test_messages_endpoint.py`. +- Agent service handler tests live in `services/oss/tests/pytest/unit/agent/`. +- Wire-contract tests live in `sdks/python/oss/tests/pytest/unit/agents/test_wire_contract.py`. +- Runner tool tests live in `services/agent/test/`. diff --git a/docs/design/agent-workflows/implementation-review.md b/docs/design/agent-workflows/implementation-review.md new file mode 100644 index 0000000000..ede0a9e903 --- /dev/null +++ b/docs/design/agent-workflows/implementation-review.md @@ -0,0 +1,159 @@ +# Implementation Review + +This is a high-level cleanup review for splitting the agent workflow work into reviewable +PRs. It avoids single-bug detail unless the detail points to a larger design risk. + +## Scope Reviewed + +- `services/oss/src/agent/` +- `sdks/python/agenta/sdk/agents/` +- `sdks/python/agenta/sdk/decorators/routing.py` +- `sdks/python/agenta/sdk/models/workflows.py` +- `services/agent/src/` +- Agent workflow design docs under this folder + +## Findings + +### Public SDK Surface Exposes A Stub Backend + +`LocalBackend` is exported from `agenta.sdk.agents`, advertised in adapter docs, and listed +as supporting Pi and Claude, but both core methods raise `NotImplementedError`. + +This blocks the standalone SDK story and anything in [sdk-local-tools/](sdk-local-tools/) +that assumes local agent execution. It should become either a real backend or a clearly +experimental import path before public docs or examples point users at it. + +### Session Protocol Is Ahead Of Persistence + +`/messages` and `/load-session` are real routes, and the session id flows through the +runtime. The durable-history side is still missing. `NoopSessionStore` is the default, and +the runtime does not save completed turns to a store. + +This is the largest architectural gap because the API shape looks done while the product +behavior is still client-held history. Keep the route, but label it clearly until storage +lands. + +There is a second session gap beyond history: future harness session snapshots. The meeting +discussion called out saving state during cleanup and loading it during setup, especially +for Rivet/ACP-style sessions. That is not the same as storing chat messages and should be a +separate design decision. + +### Agent Template Boundaries Are Not Stable Yet + +The current request shape mixes `AGENTS.md`, tools, MCP config, harness, sandbox, model, +and permissions. That is practical for the POC, but it does not yet define the persisted +agent template. + +Before the UI or storage treats templates as durable objects, split generic identity from +harness-specific options and runtime infrastructure. Runtime/sandbox selection should stay +POC-scoped unless product requirements say otherwise. + +### Streaming Crosses Several Layers + +Streaming now spans generic workflow routing, workflow request models, the Vercel adapter, +the SDK `AgentRun`, runner transports, and TypeScript engine events. The separation is +reasonable, but the blast radius is high. + +Keep contract tests around the boundaries: + +- Vercel `/messages` HTTP behavior. +- Python-to-TypeScript `/run` wire shape. +- Runner NDJSON event and terminal result records. +- Vercel stream part projection. + +### Agenta Harness Is Experimental Product Policy + +`AgentaHarness` is wired as a harness type, but its preamble, persona, and skill list are +placeholder product content. It also only works on the in-process Pi path. + +Treat this as an experimental harness. It should not be positioned as production until the +forced content is real and the unsupported rivet/Daytona path is either implemented or +hidden from config. + +### Tool Resolution Is Cleaner, But The Runtime Matrix Is Uneven + +The SDK now owns canonical tool and MCP models, while the service owns Agenta gateway and +vault adapters. That direction is healthy. The remaining runtime matrix is still uneven: + +- Code tools can run locally in the runner. +- Callback tools need `/tools/call`. +- MCP resolution is feature-gated. +- Remote MCP servers are skipped by the current runner path. +- Client tools need a browser turn boundary and cannot run headlessly. +- Named tool secrets depend on the vault resolve endpoint and failure policy. + +This needs a small matrix in every PR that changes tools, so reviewers know which +combinations are meant to work. + +The durable template contract for tools also needs tightening. The intended model is URI, +schema, and execution body or delivery reference, covering builtin Agenta tools, inline code +tools, and MCP placeholders without baking runner-specific delivery details into persisted +templates. + +### Triggers Are Missing Meaningful Work + +The June 18 discussion treated triggers as a first-class POC area. They need a source event, +a target agent/workflow, a mapping from event JSON to message or request, and lifecycle +management through a provider adapter. + +There is no trigger port, Compose.io adapter, Agenta trigger state, or event-to-agent +mapping in the current agent workflow code. This should become its own PR slice rather than +being hidden inside tool or session work. + +### MCP Is Visible Before It Is Fully Available + +The agent config schema and playground controls expose MCP server configuration. The +runtime path is narrower: service resolution is behind `AGENTA_AGENT_ENABLE_MCP`, Pi reports +no MCP capability, rivet delivers MCP only for non-Pi harnesses, and remote MCP servers are +not executed on the current runner path. + +The UI should either surface those constraints or hide MCP controls until the selected +harness/backend can honor them. + +### HITL Is Scaffolded, Not Product-Ready + +The runner has an interaction responder seam and Vercel stream projection for approval +requests, but the active responder is still a headless `auto` or `deny` policy. There is no +durable session store to hold a pending interaction across turns. + +Treat human approval, elicitation, and browser-fulfilled tools as protocol scaffolding until +the cross-turn responder and session persistence land together. + +### Historical Work-Package Labels Add Noise + +Several implementation comments still refer to WP-2, WP-7, or WP-8. Those labels helped +during the build, but they now make the current architecture harder to read. Replace them +with current names such as "runner sidecar", "callback tools", "rivet backend", or +"Vercel messages route". + +### Prompt Override Behavior Differs By Path + +Pi `systemPrompt` and `appendSystemPrompt` work on the in-process path. The rivet ACP path +logs that it ignores them. This is documented in the Pi adapter page, but it remains a +product-facing behavior difference under the same harness name. + +Decide whether to hide those fields when the selected backend cannot honor them, or keep +the warning and surface it to users. + +### Small Cleanup Candidates + +- `_normalize_tool_specs` in `sdks/python/agenta/sdk/agents/adapters/harnesses.py` appears + to be compatibility scaffolding used by tests, not the production runtime. +- Stale names from earlier iterations have already shown up in README and schema comments. + Keep scanning for similar comments before slicing PRs. + +## Suggested PR Slices + +1. Documentation and comment hygiene: current docs, trash archive, WP label cleanup. +2. Protocol hardening: `/messages` and Vercel stream tests, error behavior, headers. +3. Agent template contract: identity/config/runtime split, skills serialization, tool + contract. +4. Session persistence: real `SessionStore`, write path, ownership checks, load behavior. +5. Session snapshot design: Rivet/ACP representation, save/load lifecycle, storage choice. +6. Trigger POC: provider port, Compose.io adapter, event mapping, target invocation. +7. Agenta harness productization: real preamble, persona, skills, and config gating. +8. Local SDK backend: implement or hide `LocalBackend`. +9. Tool matrix cleanup: MCP flag behavior, remote MCP decision, client tool turn boundary, + named-secret failure behavior. +10. HITL cleanup: decide the responder contract, pending-interaction persistence, and UI + replay behavior. diff --git a/docs/design/agent-workflows/meeting-alignment.md b/docs/design/agent-workflows/meeting-alignment.md new file mode 100644 index 0000000000..64fb2126ca --- /dev/null +++ b/docs/design/agent-workflows/meeting-alignment.md @@ -0,0 +1,142 @@ +# Meeting Alignment + +This page compares the current agent workflow work with the June 18 design discussion. +It covers only the parts relevant to this folder: sessions, agent templates, tools, +runtime config, and triggers. + +## In Sync + +### Session Ids Are Body Fields + +The current `/messages` contract accepts `session_id` in the request body and returns it in +the JSON response or streaming metadata. That matches the discussion: `session_id` is an +Agenta primitive, similar in spirit to trace and span identifiers, and should not depend on +headers. + +### Missing Session Ids Are Created Implicitly + +The current route mints a `sess_` id when the client omits one. There is no separate +`create-session` endpoint. That matches the preferred direction from the meeting. + +### Known Sessions Need An Explicit Load Before The First Turn + +The `/load-session` route exists with the right shape. Once storage is implemented, a chat +client that already knows a session id should call this route before sending the first +message, otherwise it will not have history to render. + +### MCP Is Treated As Out Of POC Scope + +MCP config is present, but runtime support is narrower and feature-gated. That matches the +meeting direction: leave MCP visible as a placeholder for the representation, but do not +pretend MCP auth, secrets, and lifecycle are solved. + +### Tools Already Use The Right General Direction + +The current tool model already separates callback, code, and MCP-delivered tools, and uses +canonical schema-ish specs that the runner can execute. That is close to the meeting's +intended direction: tools should have a stable identity, schema, and execution body or +delivery mechanism. + +## Divergent Or Under-Specified + +### Unknown Client Session Ids Are Not Defined By Storage Yet + +Meeting intent: if the client supplies a session id and it exists, resume it. If the client +supplies a session id and it does not exist, create a new session using that id. + +Current code: the id is validated and echoed, but there is no durable store. In practice the +runtime cannot distinguish "new id" from "known id" yet. The docs now need to state that +the create-or-resume behavior is intended, but not implemented beyond id propagation. + +### Session Storage Is Too Narrow + +Current docs mostly describe `SessionStore` as durable chat history with `load` and +`save_turn`. The meeting discussed a second concern: saving and loading the harness session +state itself, such as a Rivet/ACP session blob, before teardown and during setup. + +That state snapshot is not implemented and is not represented clearly enough in the current +ports. Message-history persistence is enough for the MVP cold replay path. It is not enough +for future stateful resume. + +### Runtime And Sandbox Are Still Mixed Into User-Facing Config + +Meeting intent: harness selection and model belong in configuration. Runtime or sandbox +provider, such as local or Daytona, should be infrastructure for the deployed service, not a +stable part of the agent template. The POC may keep sandbox selection in config so we can +test quickly. + +Current code: `RunSelection` includes both `harness` and `sandbox`. That is acceptable for +the POC, but it should be labeled as a runtime selection, not as agent identity. + +### Agent Template Is Not Documented As A First-Class Object + +Meeting intent: an agent template starts with `AGENTS.md` plus a skills folder, serialized +into a JSON-friendly representation. Tools are part of that template. Hooks, extra code +assets, and generic permission overlays are deferred. + +Current code: `agents_md`, model, tools, MCP config, harness, sandbox, and permissions are +present as request/config fields, but the design docs did not clearly separate generic +agent identity from harness-specific and runtime-specific data. + +### Tool Representation Needs A Cleaner Contract + +Meeting intent: reuse the custom-workflow style shape where possible: URI, schema, and a +body such as inline code, external URL, or managed builtin identity. Built-in Agenta tools, +code tools, and MCP placeholders should all fit that model. + +Current code is close, but the docs do not yet say which fields are the durable template +contract versus runner-specific delivery details. This matters before we expose templates +through the UI or persist them. + +### Triggers Are Missing From This Workspace + +Meeting intent: triggers are conceptually the same class of problem as webhooks. A source +event is mapped into a target request. For the POC, Compose.io triggers should produce an +event JSON blob that can be treated as an input variable and rendered into an agent message. + +Current code/docs in this folder do not cover trigger lifecycle, event-to-agent mapping, +Agenta-owned trigger state, or the Compose.io adapter. This is meaningful missing work, not +just documentation polish. + +## Meaningful Work We Ought To Do + +### Session Representation And Lifecycle + +- Define create-or-resume semantics for supplied session ids. +- Add a storage-backed `SessionStore` for cold replay history. +- Add a separate future-facing session snapshot interface for harness state, with + `save_session` and `load_session` semantics around cleanup/setup. +- Research Rivet/ACP session representation and expected blob size before choosing + Postgres, object storage, or another backend. +- Define retention in days, not years, unless product requirements change. +- Make pre-message operations, such as file upload, use the same implicit session creation + flow and return the created `session_id`. + +### Agent Template Contract + +- Document and stabilize the template shape: `AGENTS.md`, skills, tools, and metadata. +- Keep harness/model options separate from generic identity. +- Keep sandbox/runtime provider out of persisted template identity, except for the current + POC selection field. +- Mark permissions as harness-specific and deferred until we decide whether a generic + permission overlay is worth building. +- Leave hooks, assets, and extra snippets explicitly deferred. + +### Tool Contract + +- Align tools around URI, schema, and execution body or delivery reference. +- Keep builtin Agenta tools, inline code tools, and MCP placeholders in the same conceptual + model. +- Do not claim MCP lifecycle/auth is ready. It needs its own adapter and secret story. + +### Trigger POC + +- Add a trigger port that owns subscribe, unsubscribe, and event delivery state. +- Implement a Compose.io adapter behind that port. +- Store Agenta trigger state separately from Compose.io state. +- Map the full event JSON into an agent message by default. +- Allow the user to template the message from the event using the same variable/JSON-path + mental model used by completions. +- Place trigger configuration with the agent/playground flow, even if the first POC has a + minimal UI. + diff --git a/docs/design/agent-workflows/ports-and-adapters.md b/docs/design/agent-workflows/ports-and-adapters.md index a86002c1a8..e558c8aba1 100644 --- a/docs/design/agent-workflows/ports-and-adapters.md +++ b/docs/design/agent-workflows/ports-and-adapters.md @@ -1,196 +1,153 @@ -# Ports and adapters +# Ports And Adapters -The [architecture](architecture.md) page showed the relay of programs. This page shows the -seam that keeps that relay swappable: the ports, where they live, and the adapters behind -them. +The agent runtime uses the same hexagonal vocabulary as the rest of Agenta. The SDK owns +the neutral ports and data contracts. The service and runner plug adapters into them. -## Where the runtime lives +## Runtime Package -The neutral runtime is part of the published Python SDK, at -`sdks/python/agenta/sdk/agents/`. An SDK user gets it as `agenta.sdk.agents` (with the main -types re-exported as `ag.AgentConfig`, `ag.RivetBackend`, and so on). The Agenta service -(`services/oss/src/agent/`) is a thin consumer of it: it resolves tools and secrets -server-side, threads a trace context, and runs a turn through the same ports. Nothing in the -SDK runtime calls the Agenta API, so the same code runs an agent standalone, with no Agenta -backend at all. +The SDK runtime lives under `sdks/python/agenta/sdk/agents/`. -The package follows Agenta's hexagonal vocabulary, the same words the `api/` domains use: - -| Layer | File | What it holds | +| Layer | Files | Role | | --- | --- | --- | -| DTOs | `dtos.py` | data contracts (Pydantic): `AgentConfig`, `SessionConfig`, `Message`, events, capabilities, the per-harness configs | -| Ports | `interfaces.py` | the abstract contracts: `Backend`, `Environment`, `Sandbox`, `Session`, `Harness` | -| Adapters | `adapters/` | the implementations: the backends and the harnesses | -| Utils | `utils/` | shared plumbing for the runner-backed adapters (the `/run` wire and the transports) | - -## The three layers - -The runtime is three ports stacked, lowest to highest. - -### Backend (the engine) - -A `Backend` is the engine. It declares which harnesses it can drive, owns the sandbox and -session lifecycle, and is pure plumbing: it takes an already-harness-shaped config and -launches it. It carries no "how this harness works" logic. +| DTOs | `dtos.py` | `AgentConfig`, `RunSelection`, `SessionConfig`, messages, events, capabilities, and harness-specific config models. | +| Ports | `interfaces.py` | `Backend`, `Environment`, `Sandbox`, `Session`, `Harness`, `SessionStore`. | +| Backend adapters | `adapters/in_process.py`, `adapters/rivet.py`, `adapters/local.py` | Engines that can run a harness. | +| Harness adapters | `adapters/harnesses.py` | Per-harness mapping from neutral session config to harness-specific config. | +| Browser adapter | `adapters/vercel/` | Vercel `UIMessage` input and Vercel UI Message Stream output. | +| Runner plumbing | `utils/wire.py`, `utils/ts_runner.py` | `/run` serialization and runner transports. | +| Tools and MCP | `tools/`, `mcp/` | Canonical tool and MCP config, resolution, wire models, and errors. | + +The service imports this package. The SDK must not import the service. + +## Core Ports + +### Backend + +A `Backend` is the engine. It declares `supported_harnesses`, creates sandboxes, and opens +sessions. It does not know how Pi or Claude wants tools shaped. + +Current backends: + +- `InProcessPiBackend`: implemented, supports `pi` and `agenta`, local only. +- `RivetBackend`: implemented, supports `pi` and `claude`, local or Daytona. +- `LocalBackend`: planned, public class exists, methods raise. + +### Environment + +`Environment` wraps a backend and owns sandbox policy. The default is one sandbox per +session. That is the cold isolation model. + +### Harness + +A `Harness` wraps an environment for one harness type. It validates that the backend can +drive it, maps `SessionConfig` into a harness-specific config, provisions files, and runs a +turn. + +Current harnesses: + +- `PiHarness` keeps built-in tool names, resolved tool specs, Pi prompt overrides, and Pi + native tool delivery. +- `ClaudeHarness` drops Pi built-ins, carries MCP-delivered specs, and carries the + permission policy. +- `AgentaHarness` is Pi with forced Agenta policy layered on top. + +### Session + +`Session` represents one conversation from the SDK point of view. Today it is a cold +wrapper around one `/run` call. It exposes both: + +- `prompt(...)`: one-shot path returning `AgentResult`. +- `stream(...)`: live path returning `AgentRun`. + +`AgentRun` yields live `AgentEvent` objects and exposes the terminal `AgentResult` after +the stream drains. + +### SessionStore + +`SessionStore` is the durable-history port. It has `load` and `save_turn`. The only default +adapter is `NoopSessionStore`, which returns no messages and discards writes. + +This is intentional scaffolding. Server-owned session history is not implemented yet. + +A separate future port is still needed for harness session snapshots. Durable message +history can reload a transcript, but it cannot necessarily restore Rivet/ACP session state, +tool state, or setup artifacts. That future port should be designed after we inspect the +actual session representation and storage size. + +## Config Ownership + +`AgentConfig` describes the agent itself: instructions, model, tool references, MCP server +config, and per-harness option bags. It does not choose a backend. + +`RunSelection` describes runtime choices: harness, sandbox, and permission policy. + +This is the current POC shape. The long-term split should be stricter: + +- Generic agent identity: `AGENTS.md`, skills, tool references, and metadata. +- Harness-specific config: harness id, model, option bags, and harness-specific + permissions. +- Runtime infrastructure: local versus Daytona, runner sidecar URL, filesystem isolation, + and secret channels. + +Sandbox is currently selectable through `RunSelection` so the POC can exercise local and +Daytona paths. It should not become durable agent template identity unless product +requirements explicitly need portable per-template runtime selection. + +`SessionConfig` describes one run: the neutral agent config plus resolved secrets, resolved +tools, resolved MCP servers, trace context, and the session id. + +## Service Composition + +`services/oss/src/agent/app.py` is a thin consumer of the SDK ports: + +1. Parse `AgentConfig` and `RunSelection`. +2. Resolve provider secrets. +3. Resolve tools and, when enabled, MCP servers. +4. Build `SessionConfig`. +5. Choose a backend. +6. Build the harness. +7. Run `prompt` or `stream`. + +Tool and MCP resolution are split cleanly: + +- The SDK owns canonical models, parsing, local secret provider interfaces, and generic + resolver behavior. +- The service owns Agenta-specific HTTP adapters for gateway tools and vault secrets. +- The TypeScript runner owns actual execution for callback, code, and MCP-delivered tools. + +## Browser Protocol Adapter + +The Vercel adapter is not part of the generic workflow route. It is registered only for +agent routes and lives in `sdks/python/agenta/sdk/agents/adapters/vercel/`. + +It owns: + +- Vercel `UIMessage` to neutral `Message` conversion. +- `session_id` validation and minting. +- `/messages` stream negotiation. +- Vercel stream-part encoding. +- `/load-session` over `SessionStore`. + +This keeps Vercel-specific names out of the runtime ports. + +## The `/run` Boundary + +Runner-backed backends send the same `/run` wire shape whether they use HTTP or spawn the +CLI. The Python and TypeScript sides intentionally duplicate the contract: + +- Python: `sdks/python/agenta/sdk/agents/utils/wire.py` +- TypeScript: `services/agent/src/protocol.ts` + +Golden tests pin this boundary. Any change to request fields, event kinds, capabilities, or +result fields should update both sides and the wire tests in the same PR. -```python -class Backend(ABC): - supported_harnesses: ClassVar[FrozenSet[HarnessType]] = frozenset() - def supports(self, harness) -> bool: ... - async def create_sandbox(self) -> Sandbox: ... - async def create_session(self, sandbox, config, *, harness, secrets, trace, session_id) -> Session: ... -``` +## Known Weak Points -Each backend is its own class and hard-codes what makes it that engine. There is no shared -base beyond the ABC. Three exist: - -- **`RivetBackend`** drives a harness over ACP through the TypeScript rivet runner. It - supports Pi and Claude. Its `sandbox` axis (`local` or `daytona`) is a constructor - argument, because it is a real runtime choice. -- **`InProcessPiBackend`** drives Pi in-process through the runner, with no rivet daemon. Pi - only, local only. It was the first backend and stays as the simplest one, the reference to - read when writing a new backend. -- **`LocalBackend`** runs a harness on the user's own machine for standalone SDK use (Pi via - a bundled JS runner, Claude via the Python `claude-agent-sdk`). See - [`scratch/sdk-local-backend/status.md`](scratch/sdk-local-backend/status.md) for its build - state. - -`RivetBackend` and `InProcessPiBackend` are different engines that happen to share the -`utils` wire and transport helpers; neither subclasses the other. - -### Environment (where it runs) - -An `Environment` wraps a backend and owns the sandbox policy: by default a fresh sandbox per -session (the cold model, strong isolation). Share one `Environment` across harnesses to -share its sandbox, or use one per harness to isolate them. The workflow handler builds an -`Environment(backend)` and never touches the backend's sandbox calls directly. - -### Harness (the conversation, per harness type) - -A `Harness` wraps an `Environment` for one harness type (`PiHarness`, `ClaudeHarness`). It -does two jobs. First, it validates at construction that the environment's backend can drive -it; if not, it raises `UnsupportedHarnessError` immediately: - -```python -ClaudeHarness(Environment(InProcessPiBackend())) -# UnsupportedHarnessError: InProcessPiBackend cannot drive harness 'claude'; it supports: pi -``` - -Second, it holds the per-harness adaptation logic, the part that used to live in the -TypeScript runner. `Harness._to_harness_config` maps the neutral `SessionConfig` into the -harness's own config, and the two harnesses genuinely differ: - -- **`PiHarness`** keeps built-in tool names, delivers resolved tools natively (Pi has no - MCP), and forces the permission policy to `auto` because Pi does not gate tool use. -- **`ClaudeHarness`** drops Pi built-ins (Claude has none), delivers tools over MCP, and - honors the permission policy because Claude gates tool use. - -Both normalize the resolved tool specs (a name, a description, a JSON-Schema `inputSchema`, -the `callRef`). The backend below stays pure plumbing; this layer owns the harness knowledge. - -A `make_harness(harness_type, environment)` factory maps the playground's harness string to -the right class. - -The workflow handler runs a turn through these ports: - -```python -backend = select_backend(selection) # RivetBackend or InProcessPiBackend -harness = make_harness(selection.harness, Environment(backend)) -await harness.setup() -result = await harness.prompt(session_config, messages) -await harness.cleanup() -``` - -## The configs - -`AgentConfig` is the one neutral config the platform and playground speak: instructions -(written as `AGENTS.md`), model, and provider-agnostic tool references. -`AgentConfig.from_params` parses a downloaded config dict (the `agent` element, a `prompt` -prompt-template, or a flat shape) so a standalone user runs exactly what the playground -stores. `RunSelection` carries the run-time choices stored alongside it (harness, sandbox, -permission policy); the caller reads it to pick a backend and a harness class. - -`SessionConfig` bundles everything one run needs except where it runs: the `AgentConfig`, -the provider secrets, the permission policy, the trace context, and the resolved tool -delivery (built-in names, custom specs, the `/tools/call` callback). Sandbox is deliberately -not in it; that is a backend and environment concern. - -The per-harness configs (`PiAgentConfig`, `ClaudeAgentConfig`) are what a backend plumbs. -Each shapes its own tool and permission fields for the wire, so the difference between Pi's -native tools and Claude's MCP tools lives in the config types, not in a runtime branch. - -## How the service picks a backend - -The handler chooses on every request, in `services/oss/src/agent/app.py`. `select_backend` -returns a backend instance: `InProcessPiBackend` for Pi running locally, and `RivetBackend` -otherwise (any other harness, a non-local sandbox, or `AGENTA_AGENT_RUNTIME=rivet`). The -in-process Pi engine only knows how to run Pi locally, so anything else routes to rivet -rather than silently dropping the choice. - -The transport to the runner is a deployment detail each backend takes as a constructor -argument: `AGENTA_AGENT_PI_URL` set (the Docker deployment) means HTTP to the sidecar; unset -(a local checkout) means spawn the runner CLI from the wrapper directory. - -## The wire contract: one `/run` shape - -Both transports send the same camelCase JSON to the TypeScript runner and parse the same -result back. The shape lives once in `utils/wire.py` on the Python side and `protocol.ts` on -the TypeScript side. This contract is the actual boundary of the system. - -**Request** (the harness-shaped config plus the conversation): - -| Field | Meaning | -| --- | --- | -| `backend` | The engine the runner uses (`rivet` or `pi`), set by the backend | -| `harness`, `sandbox` | The two swap axes | -| `sessionId` | Continue a prior run by replaying its history | -| `agentsMd` | The agent's instructions, written as `AGENTS.md` | -| `model` | The requested model id | -| `messages` | The conversation so far; the runner sends the latest turn and replays the rest | -| `secrets` | Provider API keys as env vars, resolved from the project vault | -| `tools`, `customTools`, `toolCallback` | The resolved runnable tools and where they call back | -| `permissionPolicy` | `auto` or `deny` for a permission-gating harness | -| `trace` | The Agenta trace context, so the run nests under the `/invoke` span | - -**Result** (the reply plus structured run metadata): - -| Field | Meaning | -| --- | --- | -| `output` | The final assistant text (what the playground renders) | -| `messages` | The structured assistant messages | -| `events` | The structured event log for the turn (see below) | -| `usage` | Token and cost totals, rolled onto the workflow span | -| `stopReason` | Why the turn ended | -| `capabilities` | What the harness was probed to support this run | -| `sessionId`, `model`, `traceId` | Identifiers for the run | - -## The shared vocabulary: capabilities, content blocks, events - -Three neutral types travel on that wire. They are ours, not any one engine's, so a non-rivet -adapter implements them too. - -**Capabilities** describe what a harness can do: `mcp_tools`, `images`, `usage`, -`streaming_deltas`, `permissions`, and the rest. The rivet runner probes them live from the -daemon and returns them in the result. This is what removed the brittle `if harness == "pi"` -branches in the runner: it now branches on a flag, where the live answer is. For example, it -delivers tools over MCP only when the harness reports `mcp_tools`. - -**Content blocks** mirror ACP: a message's content is either a plain string or a list of -`text` / `image` / `resource` blocks. Today the playground sends only text. The image and -resource kinds are plumbed through the types so an image-capable harness can take them. - -**Events** are the structured stream. Each event is one of `message`, `thought`, -`tool_call`, `tool_result`, `usage`, `error`, or `done`. The runner builds this log from the -harness as the run proceeds and returns it on the result. An `on_event` sink can also -receive them. Today the transports deliver the whole log at once after the run, since `/run` -is request-and-response; live streaming over the HTTP edge is a documented follow-on. - -## Why this shape - -The port mirrors rivet's vocabulary but keeps the types ours, so rivet is one adapter behind -the seam, not the seam itself. The same ports carry two working engines (rivet over ACP, -in-process Pi) and have room for a standalone local engine. Making the engine a real -`Backend` class, rather than a string the transport carries, is what lets a backend hard-code -its own identity and lets a standalone SDK user construct one directly. The cost of the -flexibility is one extra hop and one wire contract to keep in sync across two languages, which -the `utils/wire.py` and `protocol.ts` pairing contains in one place each. +- `LocalBackend` appears in public exports but is not usable yet. +- `SessionStore` has no production adapter and the current runtime does not call + `save_turn` after completed `/messages` turns. +- `AgentaHarness` policy content is placeholder product copy. +- `AgentaHarness` cannot run on rivet or Daytona. +- MCP server resolution is disabled unless `AGENTA_AGENT_ENABLE_MCP` is truthy. +- The code still has historical WP labels in comments. Those labels should not guide new + design decisions. diff --git a/docs/design/agent-workflows/pr-stack.md b/docs/design/agent-workflows/pr-stack.md new file mode 100644 index 0000000000..2e739d0545 --- /dev/null +++ b/docs/design/agent-workflows/pr-stack.md @@ -0,0 +1,223 @@ +# PR Stack + +This page proposes functional breakpoints for turning the current agent workflow work into +reviewable stacked PRs. Each PR should leave the repo in a coherent state and should avoid +mixing protocol, product policy, and runtime behavior unless the dependency is direct. + +## Principles + +- Keep contracts and behavior in separate PRs when possible. +- Put docs next to the behavior they explain, but avoid moving old design history in a + behavior PR. +- Prefer one runtime axis per PR: sessions, tools, harness selection, or standalone SDK. +- Each PR should include the smallest tests that prove its boundary. + +## Proposed Stack + +### 1. Documentation And Comment Hygiene + +Purpose: make the ground truth readable before reviewers look at behavior. + +Scope: + +- Keep `docs/design/agent-workflows/` organized around current implementation facts. +- Keep `trash/` as historical material only. +- Remove stale names such as old harness names, old file names, and work-package labels + from live comments and docs. + +Out of scope: runtime behavior changes. + +Validation: docs link scan, `git diff --check`, Python lint on touched comment/docstring +files. + +### 2. Protocol Shell Hardening + +Purpose: make `/messages` and streaming reviewable as an HTTP contract. + +Scope: + +- `/messages` request folding from Vercel `UIMessage` to neutral messages. +- `session_id` validation and minting. +- JSON versus Vercel SSE negotiation. +- Pre-stream failures staying JSON. +- Vercel stream part encoding. + +Out of scope: durable session storage, HITL continuation, new UI behavior. + +Validation: `sdks/python/oss/tests/pytest/utils/test_messages_endpoint.py`, Vercel stream +projection tests, wire-contract tests. + +### 3. Runner Streaming Boundary + +Purpose: isolate the Python-to-TypeScript live event transport from browser protocol work. + +Scope: + +- Runner NDJSON event/result records. +- `AgentRun` lifecycle, result handling, and cleanup. +- HTTP and subprocess streaming transports. +- Abort and cleanup behavior where already implemented. + +Out of scope: Vercel part names, session persistence, new event kinds unless required by +existing stream behavior. + +Validation: runner transport tests, `AgentRun` tests, sidecar stream tests. + +### 4. Agent Template Contract + +Purpose: stop the request shape from becoming accidental persisted product schema. + +Scope: + +- Define the durable template DTO around `AGENTS.md`, skills, tools, and metadata. +- Separate generic identity from harness-specific config and runtime infrastructure. +- Decide how skills folders are serialized, loaded, validated, and exported. +- Align tool templates around URI, schema, and execution body or delivery reference. +- Mark hooks, assets, extra snippets, and generic permission overlays as deferred. + +Out of scope: full trigger integration, storage migration for existing agents, MCP auth. + +Validation: DTO tests, schema-control tests if UI changes, import/export fixture tests. + +### 5. Cold Session Persistence + +Purpose: make `session_id` mean reloadable server-owned history while keeping cold replay. + +Scope: + +- Create-or-resume semantics for known and unknown valid session ids. +- Production `SessionStore` adapter. +- Persist completed `/messages` turns. +- Load persisted history through `/load-session`. +- Project/caller ownership checks. +- Policy for failed, cancelled, and partially streamed turns. +- Pre-message operations, such as file upload, using the same implicit session creation + path when they need a session id. + +Out of scope: warm daemon sessions, ACP `session/load`, session fork, harness state +snapshots. + +Validation: service/session tests, load-session tests, tenant access tests. + +### 6. Session Snapshot Design + +Purpose: prepare for stateful resume without blocking cold replay persistence. + +Scope: + +- Inspect Rivet/ACP session representation and blob size. +- Define save/load lifecycle around harness setup and cleanup. +- Decide storage class: database, object storage, or another session store. +- Define retention and cleanup policy. +- Keep this separate from Vercel message-history persistence. + +Out of scope: implementing warm daemon sessions unless the design proves it is small. + +Validation: design fixtures, adapter contract tests if a port is introduced. + +### 7. Trigger POC + +Purpose: prove external events can invoke agents without hard-coding one provider into the +agent runtime. + +Scope: + +- Trigger provider port with subscribe, unsubscribe, and event normalization. +- Compose.io adapter as the first provider. +- Agenta-owned trigger state and provider-state reconciliation. +- Default event-to-message mapping using the full event JSON. +- Optional message template using event context and JSON-path-style lookup. +- Target invocation into an Agenta workflow or agent. + +Out of scope: polished UX, broad provider catalog, full Automations rework. + +Validation: provider contract tests, Compose.io adapter tests with mocked API, event mapping +tests, target invocation tests. + +### 8. MCP Availability And UI Gating + +Purpose: align visible configuration with runtime support. + +Scope: + +- Decide whether MCP controls are hidden, disabled, or warning-labeled per selected + harness/backend. +- Clarify `AGENTA_AGENT_ENABLE_MCP` behavior. +- Document and test local stdio versus unsupported remote MCP paths. +- Preserve Pi's `mcpTools: false` behavior unless changing it intentionally. + +Out of scope: full remote MCP implementation unless this PR explicitly chooses to build it. + +Validation: frontend control tests if UI changes, service resolver tests, runner MCP bridge +tests. + +### 9. Tool Runtime Matrix Cleanup + +Purpose: make tool execution behavior explicit and stable. + +Scope: + +- Code tool subprocess behavior and scoped env. +- Callback tool resolution and `/tools/call` dispatch. +- Client tool non-headless behavior. +- Named-secret failure policy. +- Duplicate or compatibility helper cleanup, including `_normalize_tool_specs` if it is no + longer needed. + +Out of scope: session persistence and HITL unless client tools require a minimal contract +change. + +Validation: SDK tool resolver tests, service gateway/vault adapter tests, runner tool tests. + +### 10. Agenta Harness Productization Or Gating + +Purpose: stop the experimental `agenta` harness from looking production-ready before it is. + +Scope: + +- Replace placeholder preamble, persona, and skill list, or hide the harness. +- Gate invalid `agenta` + rivet/Daytona selections before they reach runtime failure. +- Decide whether missing forced skills should fail hard or remain soft-fail. + +Out of scope: generic Pi or Claude behavior. + +Validation: harness adapter tests, selection/gating tests, runner skill-loading tests. + +### 11. LocalBackend Implementation Or Removal From Public Surface + +Purpose: unblock or de-scope standalone SDK execution. + +Scope: + +- Implement `LocalBackend` for Pi first, or stop exporting/documenting it as usable. +- Wire bundled runner assets if implementing. +- Decide what Claude local support means and whether it belongs in the same PR. + +Out of scope: gateway tools and connected Agenta features unless needed for the chosen +minimal local path. + +Validation: SDK integration test that runs a tool-free local agent, followed by local tool +tests when tool support is added. + +### 12. HITL Continuation + +Purpose: make approval/input/client-tool interactions work across turns. + +Scope: + +- Cross-turn responder contract. +- Pending interaction persistence. +- Vercel approval/input reply folding. +- UI replay and timeout behavior. + +Out of scope: warm sessions unless required by the chosen responder model. + +Validation: responder tests, `/messages` continuation tests, frontend interaction tests. + +## Review Order + +The first three PRs are mostly contract and cleanup. They make later behavior easier to +review. Agent template work should land before broad UI persistence. Session persistence +should land before HITL. Trigger work can run in parallel with session work if it keeps its +state and provider adapter isolated. `LocalBackend` can run in parallel with session work if +it avoids shared files, but it should not depend on the agent browser protocol. diff --git a/docs/design/agent-workflows/protocol.md b/docs/design/agent-workflows/protocol.md new file mode 100644 index 0000000000..67dbf35398 --- /dev/null +++ b/docs/design/agent-workflows/protocol.md @@ -0,0 +1,141 @@ +# Protocol + +The agent workflow has two public HTTP surfaces and one internal runner surface. + +| Surface | Status | Consumer | Purpose | +| --- | --- | --- | --- | +| `POST /invoke` | Implemented | Generic workflow clients | Batch workflow call. Returns one final response. | +| `POST /messages` | Implemented | Browser chat clients | Agent chat call. Accepts Vercel `UIMessage` input and can stream Vercel SSE. | +| `POST /load-session` | Shell implemented | Browser chat clients | Loads saved session history. Returns empty history by default because storage is not wired. | +| `POST /run` | Implemented internal wire | Python SDK backend adapters | Runs one agent turn through the TypeScript runner sidecar or CLI. | + +## `/invoke` + +`/invoke` keeps the normal workflow contract. The agent handler reads messages from +`data.inputs.messages`, reads config from `data.parameters`, runs one cold turn, and returns: + +```json +{ + "role": "assistant", + "content": "..." +} +``` + +Usage is recorded on the workflow span. It is not added to the response body. + +## `/messages` + +`/messages` is registered only for agent routes. It adapts the browser chat contract to the +same runtime that `/invoke` uses. + +Request: + +```json +{ + "session_id": "sess_abc", + "data": { + "messages": [], + "inputs": {}, + "parameters": { + "agent": { + "agents_md": "...", + "model": "gpt-5.5", + "harness": "pi", + "sandbox": "local" + } + } + } +} +``` + +Important details: + +- `session_id` is optional. The server mints one when it is absent. +- Client-supplied ids must match `^[A-Za-z0-9._:-]{1,128}$`. +- The intended storage behavior is create-or-resume: a known id resumes, and a valid unknown + id creates a new session with that id. This is not observable yet because durable storage + is not implemented. +- `data.messages` is a Vercel `UIMessage[]`. The adapter folds it into neutral runtime + `Message` objects before invoking the workflow. +- `data.stream` is not a stored config value. The route sets it from the `Accept` header. + +Response modes: + +| Accept | Result | +| --- | --- | +| `application/json` or absent | A normal `WorkflowBatchResponse` with the assistant output and `session_id`. | +| `text/event-stream` | A Vercel UI Message Stream framed as SSE. | + +Pre-stream failures stay JSON even when the client asked for SSE. This matters because tool +resolution, config parsing, or auth can fail before the stream starts. + +## Vercel Stream Parts + +The runtime emits neutral `AgentEvent` objects. The Vercel adapter maps them to stream parts. + +| Agent event | Vercel part | +| --- | --- | +| `message` | `text-start`, `text-delta`, `text-end` | +| `message_start`, `message_delta`, `message_end` | Matching text lifecycle parts | +| `thought` | `reasoning-start`, `reasoning-delta`, `reasoning-end` | +| `reasoning_start`, `reasoning_delta`, `reasoning_end` | Matching reasoning lifecycle parts | +| `tool_call` | `tool-input-start`, `tool-input-available` | +| `tool_result` | `tool-output-available`, `tool-output-error`, or `tool-output-denied` | +| `interaction_request` | `tool-approval-request` or a `data-*` interaction part | +| `data` | `data-` | +| `file` | `file` | +| `usage` | `messageMetadata.usage` on `finish` | +| `error` | `error` | +| `done` | `finish-step`, then `finish` | + +The first `start` part carries `messageMetadata.sessionId`. The SSE stream ends with +`data: [DONE]`. + +## `/load-session` + +`/load-session` accepts: + +```json +{ "session_id": "sess_abc" } +``` + +It returns: + +```json +{ "session_id": "sess_abc", "messages": [] } +``` + +The route is real, but the default store is `NoopSessionStore`. Until a production +`SessionStore` is injected and completed turns call `save_turn`, the endpoint only confirms +the contract. + +Clients that already know a session id should call this endpoint before the first chat turn +if they need history on screen. The normal chat path should not require a separate explicit +create-session call. + +## `/run` + +`/run` is the internal Python-to-TypeScript boundary. The Python side serializes it in +`sdks/python/agenta/sdk/agents/utils/wire.py`. The TypeScript side mirrors it in +`services/agent/src/protocol.ts`. + +Request fields include: + +| Field | Meaning | +| --- | --- | +| `backend` | Runner engine: `pi` or `rivet`. | +| `harness` | Harness id: `pi`, `claude`, or `agenta` depending on backend support. | +| `sandbox` | Sandbox id, usually `local` or `daytona`. | +| `sessionId` | External conversation id. The runtime is still cold and receives history in `messages`. | +| `agentsMd` | Instructions that become `AGENTS.md`. | +| `systemPrompt`, `appendSystemPrompt` | Pi prompt overrides. Not delivered on the rivet Pi path yet. | +| `model` | Requested model id. | +| `messages` | Conversation history and current turn. | +| `secrets` | Provider env vars resolved by the service. | +| `tools`, `customTools`, `toolCallback`, `mcpServers` | Resolved tool delivery. | +| `permissionPolicy` | `auto` or `deny` for permission-gating harnesses. | +| `trace` | Trace context for nested spans. | + +One-shot calls return one JSON result. Streaming calls use NDJSON internally: one +`{"kind":"event"}` record per live event, followed by one `{"kind":"result"}` terminal +record. The browser never sees this NDJSON directly; `/messages` converts it to Vercel SSE. diff --git a/docs/design/agent-workflows/sdk-local-tools/README.md b/docs/design/agent-workflows/sdk-local-tools/README.md index cab7ed26d3..a9e755c33d 100644 --- a/docs/design/agent-workflows/sdk-local-tools/README.md +++ b/docs/design/agent-workflows/sdk-local-tools/README.md @@ -4,7 +4,7 @@ This folder plans one feature: letting a standalone Agenta Python SDK user run a **tools** during a fully local run, with no Agenta backend service and no rivet sidecar. It complements the sibling effort in -[`../scratch/sdk-local-backend/status.md`](../scratch/sdk-local-backend/status.md). That one +[`../trash/sdk-local-backend/status.md`](../trash/sdk-local-backend/status.md). That one moves the agent runtime into the SDK and builds `LocalBackend` (the engine that runs a harness on the user's own machine). This one builds the tool layer on top of that engine: resolving the agent's tool references into runnable specs, and supplying the secrets those @@ -36,5 +36,5 @@ the agent locally **with its tools**: built-in, code, and (later) gateway, clien ## Prerequisite This work assumes `LocalBackend` exists. It does not yet; it is a stub that raises (see -research.md). Read `../scratch/sdk-local-backend/status.md` for that effort's state. The +research.md). Read `../trash/sdk-local-backend/status.md` for that effort's state. The phases here are sequenced so the first tool slice lands right after `LocalBackend` does. diff --git a/docs/design/agent-workflows/sdk-local-tools/context.md b/docs/design/agent-workflows/sdk-local-tools/context.md index 3cadc85881..58aa6690c6 100644 --- a/docs/design/agent-workflows/sdk-local-tools/context.md +++ b/docs/design/agent-workflows/sdk-local-tools/context.md @@ -66,7 +66,7 @@ opt-in network call, and which stay out of reach for now. ## Non-goals - **Building `LocalBackend` itself.** The sibling effort - ([`../scratch/sdk-local-backend/status.md`](../scratch/sdk-local-backend/status.md)) owns + ([`../trash/sdk-local-backend/status.md`](../trash/sdk-local-backend/status.md)) owns the engine. This effort is the tool layer on top of it and assumes it exists. - **Moving gateway (Composio) execution off the server.** The provider key must stay server-side by design. A standalone gateway tool, if supported at all, calls back to diff --git a/docs/design/agent-workflows/sdk-local-tools/organization-proposal.md b/docs/design/agent-workflows/sdk-local-tools/organization-proposal.md index 1450e4fbbb..638581aa8a 100644 --- a/docs/design/agent-workflows/sdk-local-tools/organization-proposal.md +++ b/docs/design/agent-workflows/sdk-local-tools/organization-proposal.md @@ -90,7 +90,7 @@ Runtime execution lives in: - dispatches resolved tools by executor kind. - `services/agent/src/tools/code.ts` - code-tool subprocess execution and environment isolation. -- `services/agent/src/tools/client.ts` +- `services/agent/src/tools/callback.ts` - calls the Agenta callback endpoint; despite the filename, this is not a client-fulfilled tool implementation. - `services/agent/src/tools/relay.ts` @@ -484,8 +484,8 @@ targeted naming fixes: - rename `tools/execute.ts` to `tools/dispatch.ts`; the module selects an executor, while `code.ts` performs execution. -- rename `tools/client.ts` to `tools/callback.ts` or `tools/agenta-callback.ts`; “client” is - confused with the `kind: "client"` tool that the browser fulfils. +- keep the callback adapter named `tools/callback.ts`; “client” is confused with the + `kind: "client"` tool that the browser fulfils. Keep wire types in `protocol.ts` until the protocol itself is split. Moving only tool types would create cross-file protocol ownership without enough benefit. diff --git a/docs/design/agent-workflows/sdk-local-tools/plan.md b/docs/design/agent-workflows/sdk-local-tools/plan.md index 4ee5ddaf26..7f5843f14b 100644 --- a/docs/design/agent-workflows/sdk-local-tools/plan.md +++ b/docs/design/agent-workflows/sdk-local-tools/plan.md @@ -40,7 +40,7 @@ unless the user supplies a fulfiller. We document this as a limitation, not a bl service already imports the SDK's tool vocabulary (`services/oss/src/agent/tools.py:23`). When spec-building logic moves into the SDK, the service consumes it. The sibling effort locks the same rule -([`../scratch/sdk-local-backend/status.md`](../scratch/sdk-local-backend/status.md), +([`../trash/sdk-local-backend/status.md`](../trash/sdk-local-backend/status.md), "Dependency direction"). ### Decision 2: How do code and MCP executors run locally? Settled: code reuses the Pi engine; MCP is out of scope, wired but flag-gated off. @@ -119,7 +119,7 @@ The phases assume the settled decisions above. ### Phase 0 (prerequisite, owned elsewhere): `LocalBackend` runs a tool-free agent `LocalBackend` is a stub that raises (`sdks/python/agenta/sdk/agents/adapters/local.py:30`). -The sibling effort ([`../scratch/sdk-local-backend/status.md`](../scratch/sdk-local-backend/status.md)) +The sibling effort ([`../trash/sdk-local-backend/status.md`](../trash/sdk-local-backend/status.md)) must ship at least the Pi path (the bundled JS runner) before any tool work runs end to end. ### Phase 1: built-in tools, offline diff --git a/docs/design/agent-workflows/sdk-local-tools/research.md b/docs/design/agent-workflows/sdk-local-tools/research.md index 9704e3cb26..0eb389585e 100644 --- a/docs/design/agent-workflows/sdk-local-tools/research.md +++ b/docs/design/agent-workflows/sdk-local-tools/research.md @@ -135,10 +135,9 @@ target to produce. - `SessionConfig` (`sdks/python/agenta/sdk/agents/dtos.py:498`) carries `builtin_tools`, `custom_tools`, `tool_callback`, and `mcp_servers` (`:512`-`:517`). - The harness adapters in `sdks/python/agenta/sdk/agents/adapters/harnesses.py` only *shape* - already-resolved specs; they never resolve. `_normalize_tool_specs` (`:65`) fills defaults - and passes the executor fields (`kind`, `runtime`, `code`, `env`, `callRef`) through to the - wire (`:54`, `_TOOL_SPEC_PASSTHROUGH`). `PiHarness` (`:93`) keeps built-ins and delivers - specs natively; `ClaudeHarness` (`:114`) drops built-ins and routes over MCP. + already-resolved specs; they never resolve. Typed `ToolSpec` models pass executor fields + (`kind`, `runtime`, `code`, `env`, `callRef`) through to the wire. `PiHarness` keeps + built-ins and delivers specs natively; `ClaudeHarness` drops built-ins and routes over MCP. So the local resolver's job is well defined: produce the same `builtin_tools` / `custom_tools` / `tool_callback` / `mcp_servers` shapes the service produces, from local data. diff --git a/docs/design/agent-workflows/sdk-local-tools/status.md b/docs/design/agent-workflows/sdk-local-tools/status.md index 5628574334..81c59cb6de 100644 --- a/docs/design/agent-workflows/sdk-local-tools/status.md +++ b/docs/design/agent-workflows/sdk-local-tools/status.md @@ -14,7 +14,7 @@ configuration shapes are handled only at the explicit compatibility boundary in The prerequisite is also not done: `LocalBackend` is a stub that raises (`sdks/python/agenta/sdk/agents/adapters/local.py:30`). The sibling effort owns it -([`../scratch/sdk-local-backend/status.md`](../scratch/sdk-local-backend/status.md)). +([`../trash/sdk-local-backend/status.md`](../trash/sdk-local-backend/status.md)). ## Organization review (2026-06-19) diff --git a/docs/design/agent-workflows/sessions.md b/docs/design/agent-workflows/sessions.md index 20a30089b0..5c34b84aac 100644 --- a/docs/design/agent-workflows/sessions.md +++ b/docs/design/agent-workflows/sessions.md @@ -1,94 +1,129 @@ -# Sessions: today and tomorrow +# Sessions -A session is how a multi-turn conversation holds together across runs. This page explains -what a session is in the PoC today, why we built it the simple way on purpose, and the two -paths open to us tomorrow. +The agent runtime has session ids today. It does not have durable server-owned session +history yet. -## What a session is today +## Today: Cold Replay -Today a session is a `session_id` and the message history that goes with it. It is not a -live process kept warm between turns. Every turn is a fresh, cold run: the runner starts the -daemon, the adapter, and the harness, runs one turn, and tears all three down. +Each turn is cold: -Because nothing stays warm, the conversation has to be rebuilt on each turn. This works by -**replay**. The playground holds the full message history and sends it back with every turn. -The runner takes that history, flattens the prior turns into a short transcript, and puts -the transcript in front of the new message before it prompts the harness: +1. The service creates a harness session. +2. The backend sends one `/run` request to the TypeScript runner. +3. The runner starts the needed process tree. +4. The harness completes one turn. +5. The session is destroyed. -``` -Conversation so far: -user: what is the capital of France? -assistant: Paris. +Nothing warm is kept between turns. The model sees prior conversation only because the +client sends message history again. -Continue the conversation. The user now says: -and of Germany? -``` +On `/invoke`, that history is read from `data.inputs.messages`. + +On `/messages`, that history is read from `data.messages` in Vercel `UIMessage` shape, then +converted to neutral runtime messages before the same handler runs. + +## What The Session Id Does + +`session_id` is an opaque conversation id. `/messages` accepts it at the top level. If the +client omits it, the route mints one with a `sess_` prefix. If the client sends one, the +route validates the charset and length and echoes it. + +The id flows into: + +- `WorkflowInvokeRequest.session_id` +- `_agent(..., session_id=...)` +- `SessionConfig.session_id` +- the `/run` `sessionId` field +- the runner result +- the Vercel stream `start.messageMetadata.sessionId` +- the batch `WorkflowBatchResponse.session_id` + +The id groups turns, but it does not make the server authoritative for context yet. The +message history on the request is still what the model sees. + +## Intended Id Semantics + +The intended behavior is create-or-resume: + +- If the client omits `session_id`, the server creates one and returns it. +- If the client supplies a known `session_id`, the server resumes that session. +- If the client supplies an unknown but valid `session_id`, the server creates a session + using that id. + +The current implementation only validates and propagates the id. Because there is no +durable store, it cannot distinguish known from unknown ids yet. + +There should not be a required `create-session` endpoint for the normal chat path. The same +implicit creation pattern should cover pre-message operations too. For example, a file +upload before the first typed message can create a session and return the id that later +chat turns use. + +If a client already knows a session id and needs to render history, it should call +`/load-session` before sending the first message. + +## Streaming + +Streaming is implemented without changing the cold lifecycle. -The transcript is capped (by `AGENTA_AGENT_HISTORY_MAX_CHARS`) so the replayed tokens stay -bounded on long conversations. The `session_id` rides along on the trace (as `session.id` -and `gen_ai.conversation.id`) and comes back on the result, so a follow-up turn can carry -it forward. +The runner emits live NDJSON records internally. The Python `AgentRun` turns those records +into live `AgentEvent` objects. The Vercel adapter projects each event into Vercel UI +Message Stream parts and the route frames them as SSE. -## The session is already a first-class object +This means the browser can see text, reasoning, tool calls, tool results, data parts, files, +errors, and finish metadata as they happen. It does not mean the session is warm or +persisted. -Even though the lifecycle is cold, the ports model a session as a real object. The workflow -handler works through a `Harness` over an `Environment`: +## `/load-session` -```python -harness = make_harness(harness_type, Environment(backend)) -session = await harness.create_session(config) -result = await session.prompt(messages) -await session.destroy() +The route exists and calls a `SessionStore` port. The default store is `NoopSessionStore`. +It returns an empty list: + +```json +{ "session_id": "sess_abc", "messages": [] } ``` -`Session` is the conversation abstraction described on the -[ports and adapters](ports-and-adapters.md) page. Under the cold model, `prompt` sends a -fresh `/run` that replays history and `destroy` is a no-op. The abstraction is stable. Only -the mechanism behind it is cold. This matters because it gives a future session store a clean -place to attach, with no change to the handler above it. - -## Why we kept it cold on purpose - -Rivet can do real, warm sessions. Its SDK has `createSession`, `resumeSession`, and the ACP -`session/load` call, all backed by a persistence driver. The usual way to continue a -conversation is to keep one daemon warm and replay events into it with `session/load`. - -We chose not to do that yet. A warm daemon shared across runs reopens hard questions that -the cold model sidesteps: a per-session channel for secrets and trace context, and a -filesystem jail so two tenants sharing a daemon cannot read each other's files. The cold -model gives strong isolation for free, because each run is born and dies alone. For a PoC -that proves the agent workflow end to end, that trade is the right one. - -The cost is the replay above. Replay spends tokens re-sending history, and it cannot restore -in-harness state that a transcript does not capture (a partly built plan, a tool's cached -result). For short conversations this is invisible. For long or stateful ones it is the -thing a warm model would fix. - -## Tomorrow: two paths - -There are two ways to grow past cold replay, and they are not the same. - -**Path one: a server-side session store, still cold.** Keep one daemon per turn, but move -the history out of the playground and into the platform. A `SessionStore` (backed by the -backend database, or by a file for a standalone run) holds the event history. To continue, -the service replays the persisted history into a fresh cold sandbox, exactly as today, but -the platform owns the record instead of the client. This keeps the strong isolation of the -cold model and still gives durable, server-owned sessions. It is the smaller step, and the -`Session` object is already the place it attaches. - -**Path two: a warm daemon with `session/load`.** Keep a daemon alive between turns and use -the ACP `session/load` call to restore the real in-harness session, no transcript replay. -This is the richer model. It restores state a transcript cannot, and it opens the door to -`session/fork` for trying several variations of a turn. It also requires the per-session -secret channel and the filesystem jail we deferred, so it is the larger step. - -The likely order is path one first, then path two if and when stateful, long-running agents -need it. Path one is an additive feature behind the existing port. Path two is a change to -the runtime model. - -## The open question - -Path one leaves one decision for the team: where the event history lives. The default -assumption is the backend database on the platform and a file for a standalone run, which -mirrors how the rest of Agenta splits platform storage from local runs. Settling that is the -first step whenever session persistence moves from "documented" to "built." +That makes the protocol testable, but it does not restore history. A production store still +needs to be selected and wired. + +## Missing Durable History + +To make sessions real, the platform needs: + +- A production `SessionStore` implementation. +- A call to `save_turn` after each completed `/messages` turn. +- Ownership checks keyed by project and caller. +- A load path that returns persisted Vercel `UIMessage` history. +- A policy for failed, cancelled, and partially streamed turns. + +Until that lands, clients must keep sending full history. + +## Missing Session Snapshots + +Durable chat history is only the MVP path. Stateful harnesses may also need their own +session state saved before teardown and loaded during setup. This is separate from storing +Vercel `UIMessage` history. + +Examples of state that may not be recoverable from messages alone: + +- Rivet or ACP session blobs. +- Tool or harness state created during setup. +- Filesystem or process metadata needed to resume a warm-ish session after a cold restart. + +The interface is not designed yet. It likely needs explicit `save_session` and +`load_session` semantics around harness cleanup/setup, plus a storage decision after we +understand the size and shape of Rivet/ACP session data. Small JSON blobs may fit in +Postgres. Large opaque blobs may need object storage. + +Retention should be short by default, measured in days. Traces may have a different +retention policy. + +## Later: Warm Sessions + +Warm sessions are separate from durable cold history. A warm model would keep the daemon or +harness state alive and use ACP `session/load` or equivalent state restoration. That can +recover state a transcript cannot, but it also needs a filesystem jail, per-session secret +channels, and clear multi-tenant isolation. + +The likely order remains: + +1. Add server-owned history while keeping cold replay. +2. Add warm daemon sessions only if long-running stateful agents need them. diff --git a/docs/design/agent-workflows/status.md b/docs/design/agent-workflows/status.md new file mode 100644 index 0000000000..993c33f19f --- /dev/null +++ b/docs/design/agent-workflows/status.md @@ -0,0 +1,70 @@ +# Status + +## Current State + +The design folder now separates current implementation facts from old build history. +`ground-truth.md` is the source of truth for checked-in behavior. `trash/` contains the old +work-package notes and superseded streaming/session RFCs. + +The code supports batch `/invoke`, Vercel-compatible `/messages` streaming, and a +`/load-session` route shell. The runtime is still cold and replay-based. Durable session +history is not implemented. Harness session snapshots are not designed yet. + +## Progress Log + +- Moved historical `scratch/` material to `trash/`. +- Moved old streaming/session RFCs to `trash/old-rfcs/`. +- Added current `ground-truth.md`, `protocol.md`, and `implementation-review.md`. +- Added meeting-alignment, agent-template, and triggers pages to capture the June 18 design + intent that was not reflected in the current docs. +- Rewrote the top-level README, architecture, ports/adapters, and sessions pages around the + current code. +- Updated stale live comments and docs that referred to old harness names, old file names, + or old work-package labels. + +## Decisions + +- Current top-level docs describe checked-in behavior only. +- Future or blocked work must be labeled as planned, blocked, experimental, or not + implemented. +- Runtime/sandbox selection is still part of the POC request shape, but it is not durable + agent template identity unless we decide that explicitly. +- `sdk-local-tools/` remains top-level because it describes a partly implemented tool + organization and resolver effort. It is blocked by `LocalBackend`, not obsolete. +- `trash/` is historical. It is not design truth. + +## Blockers + +- `LocalBackend` is still a stub. +- `SessionStore` has no production adapter and completed turns are not persisted. +- There is no future-facing session snapshot port for Rivet/ACP or harness state. +- Trigger lifecycle and event-to-agent mapping are not implemented. +- `AgentaHarness` still uses placeholder product content and only works on the in-process + Pi path. +- MCP and HITL are visible in the protocol/config surface before the full runtime support is + finished. + +## Open Questions + +- Should `LocalBackend` remain exported before it is implemented? +- Should MCP controls be hidden or constrained by selected harness/backend? +- Should `agenta` be hidden from non-local sandbox selections? +- Which storage backend should own agent session history first? +- What is the Rivet/ACP session representation, and does it belong in Postgres, object + storage, or a separate session store? +- Should sandbox/runtime remain user-selectable after the POC, or become deployment + infrastructure only? +- What is the persisted agent template DTO that separates `AGENTS.md`, skills, tools, + harness config, and runtime selection? +- Do triggered runs always create a new session, resume one from the event, or use a + configured mapping? + +## Next Steps + +1. Run the stacked PRs suggested in `pr-stack.md`. +2. Decide whether to implement or hide `LocalBackend`. +3. Define the agent template DTO and config layering before persisting templates. +4. Build durable sessions before promising reloadable `/messages` conversations. +5. Research session snapshot representation before designing warm/stateful resume. +6. Build the trigger POC behind a port-and-adapter boundary. +7. Productize or hide the experimental Agenta harness. diff --git a/docs/design/agent-workflows/trash/README.md b/docs/design/agent-workflows/trash/README.md new file mode 100644 index 0000000000..67f4de4552 --- /dev/null +++ b/docs/design/agent-workflows/trash/README.md @@ -0,0 +1,16 @@ +# Trash + +This folder holds historical work-package notes, research spikes, proof-of-concept code, +and superseded RFCs for the agent workflow project. + +It is not the source of truth. Current design pages live one level up. Read files here only +when you need build history, old rationale, or a discarded alternative. + +## Contents + +- `wp-*`: original work-package notes. +- `research/`: raw research from the early build. +- `harness-port-redesign/`: old redesign notes that led to the current SDK ports. +- `sdk-local-backend/`: blocked local backend notes. +- `old-rfcs/`: superseded protocol RFCs. Current protocol docs live in + [../protocol.md](../protocol.md). diff --git a/docs/design/agent-workflows/scratch/harness-port-redesign/README.md b/docs/design/agent-workflows/trash/harness-port-redesign/README.md similarity index 100% rename from docs/design/agent-workflows/scratch/harness-port-redesign/README.md rename to docs/design/agent-workflows/trash/harness-port-redesign/README.md diff --git a/docs/design/agent-workflows/scratch/harness-port-redesign/implementation.md b/docs/design/agent-workflows/trash/harness-port-redesign/implementation.md similarity index 100% rename from docs/design/agent-workflows/scratch/harness-port-redesign/implementation.md rename to docs/design/agent-workflows/trash/harness-port-redesign/implementation.md diff --git a/docs/design/agent-workflows/scratch/harness-port-redesign/plan.md b/docs/design/agent-workflows/trash/harness-port-redesign/plan.md similarity index 100% rename from docs/design/agent-workflows/scratch/harness-port-redesign/plan.md rename to docs/design/agent-workflows/trash/harness-port-redesign/plan.md diff --git a/docs/design/agent-workflows/scratch/harness-port-redesign/proposal.md b/docs/design/agent-workflows/trash/harness-port-redesign/proposal.md similarity index 100% rename from docs/design/agent-workflows/scratch/harness-port-redesign/proposal.md rename to docs/design/agent-workflows/trash/harness-port-redesign/proposal.md diff --git a/docs/design/agent-workflows/scratch/harness-port-redesign/research.md b/docs/design/agent-workflows/trash/harness-port-redesign/research.md similarity index 100% rename from docs/design/agent-workflows/scratch/harness-port-redesign/research.md rename to docs/design/agent-workflows/trash/harness-port-redesign/research.md diff --git a/docs/design/agent-workflows/scratch/harness-port-redesign/status.md b/docs/design/agent-workflows/trash/harness-port-redesign/status.md similarity index 100% rename from docs/design/agent-workflows/scratch/harness-port-redesign/status.md rename to docs/design/agent-workflows/trash/harness-port-redesign/status.md diff --git a/docs/design/agent-workflows/agent-protocol-rfc.md b/docs/design/agent-workflows/trash/old-rfcs/agent-protocol-rfc.md similarity index 100% rename from docs/design/agent-workflows/agent-protocol-rfc.md rename to docs/design/agent-workflows/trash/old-rfcs/agent-protocol-rfc.md diff --git a/docs/design/agent-workflows/streaming-and-sessions.md b/docs/design/agent-workflows/trash/old-rfcs/streaming-and-sessions.md similarity index 100% rename from docs/design/agent-workflows/streaming-and-sessions.md rename to docs/design/agent-workflows/trash/old-rfcs/streaming-and-sessions.md diff --git a/docs/design/agent-workflows/scratch/research/auth-secrets.md b/docs/design/agent-workflows/trash/research/auth-secrets.md similarity index 100% rename from docs/design/agent-workflows/scratch/research/auth-secrets.md rename to docs/design/agent-workflows/trash/research/auth-secrets.md diff --git a/docs/design/agent-workflows/scratch/research/daytona-sandbox.md b/docs/design/agent-workflows/trash/research/daytona-sandbox.md similarity index 100% rename from docs/design/agent-workflows/scratch/research/daytona-sandbox.md rename to docs/design/agent-workflows/trash/research/daytona-sandbox.md diff --git a/docs/design/agent-workflows/scratch/research/diskless-in-memory-config.md b/docs/design/agent-workflows/trash/research/diskless-in-memory-config.md similarity index 100% rename from docs/design/agent-workflows/scratch/research/diskless-in-memory-config.md rename to docs/design/agent-workflows/trash/research/diskless-in-memory-config.md diff --git a/docs/design/agent-workflows/scratch/research/open-questions.md b/docs/design/agent-workflows/trash/research/open-questions.md similarity index 100% rename from docs/design/agent-workflows/scratch/research/open-questions.md rename to docs/design/agent-workflows/trash/research/open-questions.md diff --git a/docs/design/agent-workflows/scratch/research/otel-instrumentation.md b/docs/design/agent-workflows/trash/research/otel-instrumentation.md similarity index 100% rename from docs/design/agent-workflows/scratch/research/otel-instrumentation.md rename to docs/design/agent-workflows/trash/research/otel-instrumentation.md diff --git a/docs/design/agent-workflows/scratch/research/pi-interaction.md b/docs/design/agent-workflows/trash/research/pi-interaction.md similarity index 100% rename from docs/design/agent-workflows/scratch/research/pi-interaction.md rename to docs/design/agent-workflows/trash/research/pi-interaction.md diff --git a/docs/design/agent-workflows/scratch/research/sandbox-sharing.md b/docs/design/agent-workflows/trash/research/sandbox-sharing.md similarity index 100% rename from docs/design/agent-workflows/scratch/research/sandbox-sharing.md rename to docs/design/agent-workflows/trash/research/sandbox-sharing.md diff --git a/docs/design/agent-workflows/scratch/sdk-local-backend/status.md b/docs/design/agent-workflows/trash/sdk-local-backend/status.md similarity index 100% rename from docs/design/agent-workflows/scratch/sdk-local-backend/status.md rename to docs/design/agent-workflows/trash/sdk-local-backend/status.md diff --git a/docs/design/agent-workflows/scratch/wp-1-pi-tracing/README.md b/docs/design/agent-workflows/trash/wp-1-pi-tracing/README.md similarity index 100% rename from docs/design/agent-workflows/scratch/wp-1-pi-tracing/README.md rename to docs/design/agent-workflows/trash/wp-1-pi-tracing/README.md diff --git a/docs/design/agent-workflows/scratch/wp-1-pi-tracing/integrating-the-tracing-extension.md b/docs/design/agent-workflows/trash/wp-1-pi-tracing/integrating-the-tracing-extension.md similarity index 100% rename from docs/design/agent-workflows/scratch/wp-1-pi-tracing/integrating-the-tracing-extension.md rename to docs/design/agent-workflows/trash/wp-1-pi-tracing/integrating-the-tracing-extension.md diff --git a/docs/design/agent-workflows/scratch/wp-1-pi-tracing/poc/.env.example b/docs/design/agent-workflows/trash/wp-1-pi-tracing/poc/.env.example similarity index 100% rename from docs/design/agent-workflows/scratch/wp-1-pi-tracing/poc/.env.example rename to docs/design/agent-workflows/trash/wp-1-pi-tracing/poc/.env.example diff --git a/docs/design/agent-workflows/scratch/wp-1-pi-tracing/poc/README.md b/docs/design/agent-workflows/trash/wp-1-pi-tracing/poc/README.md similarity index 100% rename from docs/design/agent-workflows/scratch/wp-1-pi-tracing/poc/README.md rename to docs/design/agent-workflows/trash/wp-1-pi-tracing/poc/README.md diff --git a/docs/design/agent-workflows/scratch/wp-1-pi-tracing/poc/agenta-otel.ts b/docs/design/agent-workflows/trash/wp-1-pi-tracing/poc/agenta-otel.ts similarity index 100% rename from docs/design/agent-workflows/scratch/wp-1-pi-tracing/poc/agenta-otel.ts rename to docs/design/agent-workflows/trash/wp-1-pi-tracing/poc/agenta-otel.ts diff --git a/docs/design/agent-workflows/scratch/wp-1-pi-tracing/poc/package.json b/docs/design/agent-workflows/trash/wp-1-pi-tracing/poc/package.json similarity index 100% rename from docs/design/agent-workflows/scratch/wp-1-pi-tracing/poc/package.json rename to docs/design/agent-workflows/trash/wp-1-pi-tracing/poc/package.json diff --git a/docs/design/agent-workflows/scratch/wp-1-pi-tracing/poc/pnpm-lock.yaml b/docs/design/agent-workflows/trash/wp-1-pi-tracing/poc/pnpm-lock.yaml similarity index 100% rename from docs/design/agent-workflows/scratch/wp-1-pi-tracing/poc/pnpm-lock.yaml rename to docs/design/agent-workflows/trash/wp-1-pi-tracing/poc/pnpm-lock.yaml diff --git a/docs/design/agent-workflows/scratch/wp-1-pi-tracing/poc/run.ts b/docs/design/agent-workflows/trash/wp-1-pi-tracing/poc/run.ts similarity index 100% rename from docs/design/agent-workflows/scratch/wp-1-pi-tracing/poc/run.ts rename to docs/design/agent-workflows/trash/wp-1-pi-tracing/poc/run.ts diff --git a/docs/design/agent-workflows/scratch/wp-1-pi-tracing/tracing-in-the-agent-service.md b/docs/design/agent-workflows/trash/wp-1-pi-tracing/tracing-in-the-agent-service.md similarity index 100% rename from docs/design/agent-workflows/scratch/wp-1-pi-tracing/tracing-in-the-agent-service.md rename to docs/design/agent-workflows/trash/wp-1-pi-tracing/tracing-in-the-agent-service.md diff --git a/docs/design/agent-workflows/scratch/wp-2-agent-service/README.md b/docs/design/agent-workflows/trash/wp-2-agent-service/README.md similarity index 100% rename from docs/design/agent-workflows/scratch/wp-2-agent-service/README.md rename to docs/design/agent-workflows/trash/wp-2-agent-service/README.md diff --git a/docs/design/agent-workflows/scratch/wp-2-agent-service/implementation-plan.md b/docs/design/agent-workflows/trash/wp-2-agent-service/implementation-plan.md similarity index 100% rename from docs/design/agent-workflows/scratch/wp-2-agent-service/implementation-plan.md rename to docs/design/agent-workflows/trash/wp-2-agent-service/implementation-plan.md diff --git a/docs/design/agent-workflows/scratch/wp-2-agent-service/qa.md b/docs/design/agent-workflows/trash/wp-2-agent-service/qa.md similarity index 100% rename from docs/design/agent-workflows/scratch/wp-2-agent-service/qa.md rename to docs/design/agent-workflows/trash/wp-2-agent-service/qa.md diff --git a/docs/design/agent-workflows/scratch/wp-3-daytona-sandbox/README.md b/docs/design/agent-workflows/trash/wp-3-daytona-sandbox/README.md similarity index 100% rename from docs/design/agent-workflows/scratch/wp-3-daytona-sandbox/README.md rename to docs/design/agent-workflows/trash/wp-3-daytona-sandbox/README.md diff --git a/docs/design/agent-workflows/scratch/wp-3-daytona-sandbox/poc/README.md b/docs/design/agent-workflows/trash/wp-3-daytona-sandbox/poc/README.md similarity index 100% rename from docs/design/agent-workflows/scratch/wp-3-daytona-sandbox/poc/README.md rename to docs/design/agent-workflows/trash/wp-3-daytona-sandbox/poc/README.md diff --git a/docs/design/agent-workflows/scratch/wp-3-daytona-sandbox/poc/bench_coldstart.py b/docs/design/agent-workflows/trash/wp-3-daytona-sandbox/poc/bench_coldstart.py similarity index 100% rename from docs/design/agent-workflows/scratch/wp-3-daytona-sandbox/poc/bench_coldstart.py rename to docs/design/agent-workflows/trash/wp-3-daytona-sandbox/poc/bench_coldstart.py diff --git a/docs/design/agent-workflows/scratch/wp-3-daytona-sandbox/poc/build_snapshot.py b/docs/design/agent-workflows/trash/wp-3-daytona-sandbox/poc/build_snapshot.py similarity index 100% rename from docs/design/agent-workflows/scratch/wp-3-daytona-sandbox/poc/build_snapshot.py rename to docs/design/agent-workflows/trash/wp-3-daytona-sandbox/poc/build_snapshot.py diff --git a/docs/design/agent-workflows/scratch/wp-3-daytona-sandbox/poc/cleanup.py b/docs/design/agent-workflows/trash/wp-3-daytona-sandbox/poc/cleanup.py similarity index 100% rename from docs/design/agent-workflows/scratch/wp-3-daytona-sandbox/poc/cleanup.py rename to docs/design/agent-workflows/trash/wp-3-daytona-sandbox/poc/cleanup.py diff --git a/docs/design/agent-workflows/scratch/wp-3-daytona-sandbox/poc/run_agent.py b/docs/design/agent-workflows/trash/wp-3-daytona-sandbox/poc/run_agent.py similarity index 100% rename from docs/design/agent-workflows/scratch/wp-3-daytona-sandbox/poc/run_agent.py rename to docs/design/agent-workflows/trash/wp-3-daytona-sandbox/poc/run_agent.py diff --git a/docs/design/agent-workflows/scratch/wp-4-multi-message-output/README.md b/docs/design/agent-workflows/trash/wp-4-multi-message-output/README.md similarity index 100% rename from docs/design/agent-workflows/scratch/wp-4-multi-message-output/README.md rename to docs/design/agent-workflows/trash/wp-4-multi-message-output/README.md diff --git a/docs/design/agent-workflows/scratch/wp-5-chat-vs-completion/README.md b/docs/design/agent-workflows/trash/wp-5-chat-vs-completion/README.md similarity index 100% rename from docs/design/agent-workflows/scratch/wp-5-chat-vs-completion/README.md rename to docs/design/agent-workflows/trash/wp-5-chat-vs-completion/README.md diff --git a/docs/design/agent-workflows/scratch/wp-6-workflow-type-and-template/README.md b/docs/design/agent-workflows/trash/wp-6-workflow-type-and-template/README.md similarity index 100% rename from docs/design/agent-workflows/scratch/wp-6-workflow-type-and-template/README.md rename to docs/design/agent-workflows/trash/wp-6-workflow-type-and-template/README.md diff --git a/docs/design/agent-workflows/scratch/wp-7-tools/README.md b/docs/design/agent-workflows/trash/wp-7-tools/README.md similarity index 100% rename from docs/design/agent-workflows/scratch/wp-7-tools/README.md rename to docs/design/agent-workflows/trash/wp-7-tools/README.md diff --git a/docs/design/agent-workflows/scratch/wp-8-rivet-acp-runtime/README.md b/docs/design/agent-workflows/trash/wp-8-rivet-acp-runtime/README.md similarity index 100% rename from docs/design/agent-workflows/scratch/wp-8-rivet-acp-runtime/README.md rename to docs/design/agent-workflows/trash/wp-8-rivet-acp-runtime/README.md diff --git a/docs/design/agent-workflows/scratch/wp-8-rivet-acp-runtime/architecture.md b/docs/design/agent-workflows/trash/wp-8-rivet-acp-runtime/architecture.md similarity index 100% rename from docs/design/agent-workflows/scratch/wp-8-rivet-acp-runtime/architecture.md rename to docs/design/agent-workflows/trash/wp-8-rivet-acp-runtime/architecture.md diff --git a/docs/design/agent-workflows/scratch/wp-8-rivet-acp-runtime/context.md b/docs/design/agent-workflows/trash/wp-8-rivet-acp-runtime/context.md similarity index 100% rename from docs/design/agent-workflows/scratch/wp-8-rivet-acp-runtime/context.md rename to docs/design/agent-workflows/trash/wp-8-rivet-acp-runtime/context.md diff --git a/docs/design/agent-workflows/scratch/wp-8-rivet-acp-runtime/isolation-and-fork.md b/docs/design/agent-workflows/trash/wp-8-rivet-acp-runtime/isolation-and-fork.md similarity index 100% rename from docs/design/agent-workflows/scratch/wp-8-rivet-acp-runtime/isolation-and-fork.md rename to docs/design/agent-workflows/trash/wp-8-rivet-acp-runtime/isolation-and-fork.md diff --git a/docs/design/agent-workflows/scratch/wp-8-rivet-acp-runtime/plan.md b/docs/design/agent-workflows/trash/wp-8-rivet-acp-runtime/plan.md similarity index 100% rename from docs/design/agent-workflows/scratch/wp-8-rivet-acp-runtime/plan.md rename to docs/design/agent-workflows/trash/wp-8-rivet-acp-runtime/plan.md diff --git a/docs/design/agent-workflows/scratch/wp-8-rivet-acp-runtime/poc/build_rivet_snapshot.py b/docs/design/agent-workflows/trash/wp-8-rivet-acp-runtime/poc/build_rivet_snapshot.py similarity index 69% rename from docs/design/agent-workflows/scratch/wp-8-rivet-acp-runtime/poc/build_rivet_snapshot.py rename to docs/design/agent-workflows/trash/wp-8-rivet-acp-runtime/poc/build_rivet_snapshot.py index 5e85e7b491..1b27067f98 100644 --- a/docs/design/agent-workflows/scratch/wp-8-rivet-acp-runtime/poc/build_rivet_snapshot.py +++ b/docs/design/agent-workflows/trash/wp-8-rivet-acp-runtime/poc/build_rivet_snapshot.py @@ -2,7 +2,7 @@ # requires-python = ">=3.11" # dependencies = ["daytona"] # /// -"""Build a Daytona snapshot for the WP-8 rivet agent runtime. +"""Build a Daytona snapshot for the WP-8 sandbox-agent runtime. Bakes the `pi` CLI into rivet's `-full` image (which already ships the sandbox-agent daemon, the Claude CLI, and CA certs) so Daytona runs don't pay a ~150s per-invoke @@ -12,6 +12,20 @@ AGENTA_RIVET_DAYTONA_INSTALL_PI=false Run: DAYTONA_API_KEY=... DAYTONA_TARGET=eu uv run build_rivet_snapshot.py [--force] + +Licensing (see services/agent/docker/README.md): + This script is the build recipe we ship, NOT a snapshot we distribute. Whoever + runs it builds the snapshot in their own Daytona account: Agenta Cloud builds + its own for internal use; self-hosters build their own. We never hand anyone a + Claude-containing image, so this is compliant even though the `-full` base bundles + Claude (Anthropic's Commercial Terms forbid us *distributing* Claude Code, not + building/using it). + + Cleaner-provenance follow-up (needs a live Daytona build to verify): base on a + daemon-only rivet image and install Claude from Anthropic at build (npm + `@anthropic-ai/claude-code` or `claude.ai/install.sh`), so the snapshot's Claude + comes straight from Anthropic instead of from a third party's bundled image. Pin + that only after confirming the daemon-only tag also ships the ACP adapters. """ import sys diff --git a/docs/design/agent-workflows/scratch/wp-8-rivet-acp-runtime/poc/commit_agent_config.py b/docs/design/agent-workflows/trash/wp-8-rivet-acp-runtime/poc/commit_agent_config.py similarity index 100% rename from docs/design/agent-workflows/scratch/wp-8-rivet-acp-runtime/poc/commit_agent_config.py rename to docs/design/agent-workflows/trash/wp-8-rivet-acp-runtime/poc/commit_agent_config.py diff --git a/docs/design/agent-workflows/scratch/wp-8-rivet-acp-runtime/poc/debug-events.ts b/docs/design/agent-workflows/trash/wp-8-rivet-acp-runtime/poc/debug-events.ts similarity index 100% rename from docs/design/agent-workflows/scratch/wp-8-rivet-acp-runtime/poc/debug-events.ts rename to docs/design/agent-workflows/trash/wp-8-rivet-acp-runtime/poc/debug-events.ts diff --git a/docs/design/agent-workflows/scratch/wp-8-rivet-acp-runtime/poc/dump-full.ts b/docs/design/agent-workflows/trash/wp-8-rivet-acp-runtime/poc/dump-full.ts similarity index 100% rename from docs/design/agent-workflows/scratch/wp-8-rivet-acp-runtime/poc/dump-full.ts rename to docs/design/agent-workflows/trash/wp-8-rivet-acp-runtime/poc/dump-full.ts diff --git a/docs/design/agent-workflows/scratch/wp-8-rivet-acp-runtime/poc/package.json b/docs/design/agent-workflows/trash/wp-8-rivet-acp-runtime/poc/package.json similarity index 100% rename from docs/design/agent-workflows/scratch/wp-8-rivet-acp-runtime/poc/package.json rename to docs/design/agent-workflows/trash/wp-8-rivet-acp-runtime/poc/package.json diff --git a/docs/design/agent-workflows/scratch/wp-8-rivet-acp-runtime/poc/spike.ts b/docs/design/agent-workflows/trash/wp-8-rivet-acp-runtime/poc/spike.ts similarity index 100% rename from docs/design/agent-workflows/scratch/wp-8-rivet-acp-runtime/poc/spike.ts rename to docs/design/agent-workflows/trash/wp-8-rivet-acp-runtime/poc/spike.ts diff --git a/docs/design/agent-workflows/scratch/wp-8-rivet-acp-runtime/research.md b/docs/design/agent-workflows/trash/wp-8-rivet-acp-runtime/research.md similarity index 100% rename from docs/design/agent-workflows/scratch/wp-8-rivet-acp-runtime/research.md rename to docs/design/agent-workflows/trash/wp-8-rivet-acp-runtime/research.md diff --git a/docs/design/agent-workflows/scratch/wp-8-rivet-acp-runtime/status.md b/docs/design/agent-workflows/trash/wp-8-rivet-acp-runtime/status.md similarity index 100% rename from docs/design/agent-workflows/scratch/wp-8-rivet-acp-runtime/status.md rename to docs/design/agent-workflows/trash/wp-8-rivet-acp-runtime/status.md diff --git a/docs/design/agent-workflows/triggers.md b/docs/design/agent-workflows/triggers.md new file mode 100644 index 0000000000..18ac0970b7 --- /dev/null +++ b/docs/design/agent-workflows/triggers.md @@ -0,0 +1,69 @@ +# Triggers + +Triggers are planned. They are not implemented in the current agent workflow code. + +## Concept + +A trigger connects an external source event to an Agenta target. This is the same class of +problem as webhooks: + +| Source | Event | Target | +| --- | --- | --- | +| Agenta webhook source | Agenta event JSON | HTTP destination | +| Compose.io trigger source | Compose.io event JSON | Agenta workflow or agent message | + +The POC should start with Compose.io because it gives us real event sources and lifecycle +APIs. The abstraction should not bake Compose.io into the core model. + +## Event To Agent Mapping + +The useful default is to pass the full event JSON as the message body or as an input +variable available to the message template. + +Then the user can override the message shape by templating from the event, using the same +mental model as completion variables: + +```text +New issue from {{event.repository.full_name}}: +{{event.issue.title}} +``` + +The first implementation can keep this simple: + +- Default message: the whole event JSON. +- Optional message template: renders from the event context. +- No hard-coded GitHub, Gmail, Linear, Slack, or PostHog mappings in the core agent path. + +## Lifecycle + +The platform needs two layers of state: + +- Agenta state: which project, agent/workflow, target, session policy, and message template + this trigger belongs to. +- Provider state: the Compose.io subscription id, connection/account identity, and provider + lifecycle metadata. + +That implies a port-and-adapter shape: + +- `TriggerProvider` or equivalent port with `subscribe`, `unsubscribe`, `list`, and event + normalization operations. +- `ComposeTriggerProvider` as the first adapter. +- Agenta service state that can survive provider retries and reconcile subscriptions. + +## UI Placement + +Trigger configuration belongs near the agent/playground flow because the user is wiring an +event into a specific agent behavior. It can reuse concepts from global Automations, but it +should not be hidden only under global settings. + +## Missing Work + +- Define the trigger DTOs and storage model. +- Build the Compose.io lifecycle POC. +- Define event delivery into `/messages`, `/invoke`, or a dedicated internal target. +- Decide session behavior for triggered runs: always new session, fixed session id from the + event, or user-configured mapping. +- Add the event template renderer and validation. +- Add UI affordances for connection, source event selection, target agent selection, and + test delivery. + diff --git a/docs/design/vault-named-secrets/README.md b/docs/design/vault-named-secrets/README.md new file mode 100644 index 0000000000..98b40032b5 --- /dev/null +++ b/docs/design/vault-named-secrets/README.md @@ -0,0 +1,25 @@ +# Vault named secrets + +Add arbitrary user-named secrets to the project model vault. Today the vault only stores +LLM provider keys and custom providers. This feature lets a user save any `name -> value` +secret (for example `GITHUB_TOKEN`) alongside the existing provider keys, scoped to the +project, and manage them from Settings. + +Scope for this iteration: **storage + management UI only**. We do not wire these secrets +into the agent runtime / sandbox yet. That is a separate follow-up. + +## Files in this folder + +- `context.md` — why this work exists, goals, non-goals, decisions. +- `research.md` — how the vault works today, with exact file:line references and gotchas. +- `plan.md` — the execution plan (backend, migration, frontend), phase by phase. +- `status.md` — current progress and the source of truth for what is done / next. + +## Quick orientation + +- Backend vault domain: `api/oss/src/core/secrets/` + `api/oss/src/dbs/postgres/secrets/` + + router `api/oss/src/apis/fastapi/vault/router.py`. +- Frontend vault UI: `web/oss/src/components/pages/settings/Secrets/` + entity package + `web/packages/agenta-entities/src/secret/`. +- The vault is `kind`-driven. We add one new `SecretKind` (`custom_secret`) and reuse the + generic CRUD, encryption, DAO, and mapping layers unchanged. diff --git a/docs/design/vault-named-secrets/context.md b/docs/design/vault-named-secrets/context.md new file mode 100644 index 0000000000..0b04dd500b --- /dev/null +++ b/docs/design/vault-named-secrets/context.md @@ -0,0 +1,41 @@ +# Context + +## Why this exists + +The model vault (Settings -> "Providers & Models") only stores two shapes of secret +today: standard LLM provider keys (`OPENAI_API_KEY` and friends) and custom providers. +Users want to store other named secrets in the same vault, with any name they choose and a +single value, so the project has one place for its credentials. + +The motivating downstream use is the agent workflows project: agents and their tools need +arbitrary credentials (a `GITHUB_TOKEN`, a `STRIPE_KEY`, an internal API token) that the +provider-key model cannot express. But that consumption path is explicitly out of scope +here. This iteration only adds the ability to store and manage these secrets. + +## Goals + +- Let a user create, edit, and delete a secret that is just a `name -> value` pair. +- Store it in the existing project vault, encrypted at rest like every other secret. +- Surface it in Settings as a third section next to standard and custom providers. + +## Non-goals (this iteration) + +- Do **not** inject these secrets into the agent runtime, sandbox, or any invocation. + `services/oss/src/agent/secrets.py` and `get_user_llm_providers_secrets` stay untouched. +- No per-agent / per-revision scoping. These live in the project vault, same as provider + keys. +- No new permission types. Reuse the existing `VIEW_SECRET` / `EDIT_SECRET` checks that + already guard the vault router in EE. + +## Key decisions + +- **New `SecretKind = "custom_secret"`** rather than overloading `custom_provider`. Keeps + validation, UI filtering, and any future runtime consumption unambiguous. +- **Reuse the generic vault stack.** The DAO, service, router, encryption (`PGPString`), + and DTO<->DBE mappings are all `kind`-agnostic. The only backend code that changes is + the enum, the DTO union + validator, and an Alembic enum migration. +- **Data shape mirrors the webhook secret**: `data = {secret: {key: ""}}`, with the + user-chosen name carried in `header.name`. This parallels `WebhookProviderDTO` + (`provider.key`) so it sits naturally in the existing `Union`. +- **Frontend reuses `LlmProvider`** as the row model (`name`, `key`, `id`, `type`, + `created_at`), so no new shared type is needed. diff --git a/docs/design/vault-named-secrets/plan.md b/docs/design/vault-named-secrets/plan.md new file mode 100644 index 0000000000..102e5ffb7b --- /dev/null +++ b/docs/design/vault-named-secrets/plan.md @@ -0,0 +1,164 @@ +# Execution plan + +Three phases: backend DTO + enum, DB migration, frontend. Backend and migration land +together. Frontend lands after the Fern client is regenerated. + +## Phase 1 — Backend: new secret kind + +### 1a. Add the enum member + +`api/oss/src/core/secrets/enums.py` + +```python +class SecretKind(str, Enum): + PROVIDER_KEY = "provider_key" + CUSTOM_PROVIDER = "custom_provider" + SSO_PROVIDER = "sso_provider" + WEBHOOK_PROVIDER = "webhook_provider" + CUSTOM_SECRET = "custom_secret" # NEW: arbitrary name -> value +``` + +### 1b. Add the DTO and wire it into the union + validator + +`api/oss/src/core/secrets/dtos.py` — mirror the webhook shape. + +```python +class CustomSecretSettingsDTO(BaseModel): + key: str # the secret value; encrypted at rest by the data column + +class CustomSecretDTO(BaseModel): + secret: CustomSecretSettingsDTO +``` + +- Add `CustomSecretDTO` to `SecretDTO.data: Union[...]` (`dtos.py:70`). The members stay + an untagged union; `{secret: {...}}` is distinct from the provider shapes + (`{provider: {...}}` / `{kind, provider}`), so resolution is unambiguous. +- Add a branch to `validate_secret_data_based_on_kind` (`dtos.py:147`, before the final + `else`): + +```python +elif kind == SecretKind.CUSTOM_SECRET.value: + if not isinstance(data, dict): + raise ValueError("Invalid data for CustomSecretDTO") + secret = data.get("secret") + if not isinstance(secret, dict) or "key" not in secret: + raise ValueError("CustomSecretDTO requires data.secret.key") +``` + +No changes to `CreateSecretDTO`, `UpdateSecretDTO`, `SecretResponseDTO`, mappings, DAO, +service, or router. They are all kind-agnostic. + +### 1c. Format + lint + +From `api/`: `ruff format` then `ruff check --fix`. Fix all errors. + +## Phase 2 — DB migration (both trees) + +Add one Alembic revision to **both** `api/oss/databases/postgres/migrations/core/versions/` +and `api/ee/databases/postgres/migrations/core/versions/`, chained from the current head +(`b3c4d5e6f7a9` at planning time — re-confirm first). + +```python +def upgrade() -> None: + op.execute("ALTER TYPE secretkind_enum ADD VALUE IF NOT EXISTS 'CUSTOM_SECRET'") + +def downgrade() -> None: + # PG cannot drop an enum value; no-op (matches the webhook/SSO migrations). + pass +``` + +Use the uppercase member NAME `CUSTOM_SECRET`, exactly like +`f0a1b2c3d4e5_add_webhooks.py:24`. Give the OSS and EE copies the same `revision` id so the +two trees stay in lockstep. + +## Phase 3 — Regenerate the Fern client + +After Phase 1 merges (OpenAPI now includes `custom_secret` + `CustomSecretDto`): + +``` +bash ./clients/scripts/generate.sh --language typescript +pnpm install # rebuilds @agentaai/api-client dist/ so consumers see the new types +``` + +Confirm `SecretKind.CustomSecret` and a `CustomSecretDto` alias are available from +`@agentaai/api-client`. + +## Phase 4 — Frontend: data layer + UI + +### 4a. Types — `web/packages/agenta-entities/src/secret/core/types.ts` + +Add an alias next to the others: `export type CustomSecretDto = AgentaApi.CustomSecretDto`. + +### 4b. Transforms — `web/packages/agenta-entities/src/secret/core/transforms.ts` + +Add a branch in `transformSecret` (after the `CustomProvider` branch): + +```ts +} else if (secret.kind === SecretKind.CustomSecret) { + const data = secret.data as CustomSecretDto + acc.push({ + name: secret.header.name ?? "", + key: data.secret.key, + id: secret.id ?? undefined, + type: secret.kind, + created_at: secret.lifecycle?.created_at ?? undefined, + }) +} +``` + +Add a payload builder: + +```ts +export const transformCustomSecretPayloadData = (values: LlmProvider): CreateSecretDto => ({ + header: {name: values.name, description: values.name}, + secret: { + kind: SecretKind.CustomSecret, + data: {secret: {key: values.key ?? ""}}, + }, +}) +``` + +### 4c. Atoms — `web/packages/agenta-entities/src/secret/state/atoms.ts` + +- `customNamedSecretsAtom`: filter `vaultSecretsQueryAtom.data` by + `secret.type === SecretKind.CustomSecret` (clone `customSecretsAtom` `:130`). +- `createCustomNamedSecretAtom`: clone `createCustomSecretAtom` (`:229`) but build the + payload with `transformCustomSecretPayloadData` and match existing rows by `id` + (create vs update via `updateMutation` / `createMutation`). +- Reuse `deleteSecretAtom` for delete. + +### 4d. Hook — `web/packages/agenta-entities/src/secret/state/useVaultSecret.ts` + +Expose `namedSecrets: customNamedSecretsAtom` and +`handleModifyNamedSecret(provider)` (calls the new action atom then `vaultQuery.refetch()`, +exactly like `handleModifyCustomVaultSecret` `:81`). Reuse `handleDeleteVaultSecret`. + +### 4e. UI — new table + modal under `web/oss/src/components/pages/settings/Secrets/` + +Do not overload `SecretProviderTable` (provider-specific). Add: + +- `NamedSecretTable/index.tsx`: an antd `Table` with columns Name, Value (masked, reuse the + `key.slice(0,3) + "..." + key.slice(-3)` masking from `SecretProviderTable` `:54`), + Created at, and edit/delete actions. A "Create" button opens the modal. Reads + `namedSecrets` + `loading` from `useVaultSecret`. +- `ConfigureSecretModal/index.tsx`: `EnhancedModal` (from `@agenta/ui`, per frontend + conventions) with two `LabelInput`s — Name (text) and Value (password). On submit calls + `handleModifyNamedSecret({name, key, id})`. Reuse `DeleteProviderModal` for delete (it is + already generic over `LlmProvider.id`). + +Mount it in `Secrets.tsx`: + +```tsx + + + +``` + +### 4f. Lint + +From `web/`: `pnpm lint-fix`. + +## Out of scope (explicit) + +No edits to `services/oss/src/agent/secrets.py`, `SessionConfig.secrets`, the wire +protocol, or `get_user_llm_providers_secrets`. Runtime injection is a separate follow-up. diff --git a/docs/design/vault-named-secrets/research.md b/docs/design/vault-named-secrets/research.md new file mode 100644 index 0000000000..ba41f27445 --- /dev/null +++ b/docs/design/vault-named-secrets/research.md @@ -0,0 +1,91 @@ +# Research: how the vault works today + +All references verified against the repo at planning time. + +## Backend: the vault is one generic, kind-driven store + +- Enum: `api/oss/src/core/secrets/enums.py:4` — `SecretKind` has four members today + (`provider_key`, `custom_provider`, `sso_provider`, `webhook_provider`). +- DTOs: `api/oss/src/core/secrets/dtos.py` + - `SecretDTO` (`:68`) holds `kind` plus a `data: Union[...]` over the four provider DTOs. + - `validate_secret_data_based_on_kind` (`:77`) is a `mode="before"` validator with one + `if/elif` branch per kind. Anything not matched raises "not a valid SecretKind enum". + - `WebhookProviderDTO` (`:64`) / `WebhookProviderSettingsDTO` (`:60`) is the closest + existing shape: `data = {provider: {key: str}}`. Our new kind copies this idea. + - `CreateSecretDTO` (`:153`) = `{header: Header, secret: SecretDTO}`; + `UpdateSecretDTO` (`:189`) makes both optional. `SecretResponseDTO` (`:206`) adds id + + lifecycle and a `build_up_model_keys` validator that only touches `custom_provider`. +- DBE / column: `api/oss/src/dbs/postgres/secrets/dbas.py:22` + - `kind = Column(SQLEnum(SecretKind, name="secretkind_enum"))` — a **native Postgres enum + type** named `secretkind_enum`. Adding a member needs a DB migration (see below). + - `data = Column(PGPString())` — encrypted at rest via `pgp_sym_encrypt/decrypt` + (`api/oss/src/dbs/postgres/secrets/custom_fields.py`). Kind-agnostic. +- Mapping is generic: `api/oss/src/dbs/postgres/secrets/mappings.py` + - write (`:16`) stores `kind=secret.kind.value` and `data=json.dumps(data.model_dump(...))`. + - read (`:58`) does `SecretKind(dbe.kind)` and `data=json.loads(dbe.data)`. + - **No per-kind logic.** A new kind flows through untouched. +- DAO + service are kind-agnostic too: `api/oss/src/dbs/postgres/secrets/dao.py`, + `api/oss/src/core/secrets/services.py` (sets the crypt key from `env.agenta.crypt_key`). +- Router (CRUD): `api/oss/src/apis/fastapi/vault/router.py`, mounted at `/vault/v1` + (`api/entrypoints/routers.py:758`). `POST/GET/PUT/DELETE /secrets`. EE adds + `VIEW_SECRET` / `EDIT_SECRET` permission checks and list caching. No change needed. + +### Migration gotcha (important) + +`secretkind_enum` is a native PG enum, so a new value needs `ALTER TYPE`. Precedent: +- `.../core/versions/f0a1b2c3d4e5_add_webhooks.py:24` + → `op.execute("ALTER TYPE secretkind_enum ADD VALUE IF NOT EXISTS 'WEBHOOK_PROVIDER'")` +- `.../core/versions/c3b2a1d4e5f6_add_secret_org_scope.py:25` does the same for `SSO_PROVIDER`. + +Two facts to copy exactly: +1. The label added is the **uppercase member NAME** (`CUSTOM_SECRET`), even though the + stored `.value` is lowercase. That is how the existing four members already work, so + mirror it. +2. Migrations are **duplicated in both trees**: `api/oss/databases/postgres/migrations/core/versions/` + and `api/ee/databases/postgres/migrations/core/versions/`. Add the new revision to both. +3. Current core head at planning time: **`b3c4d5e6f7a9`** + (`b3c4d5e6f7a9_repair_workflow_revision_versions.py`). Re-confirm the head before writing + `down_revision` (compute by elimination over the `versions/` dir). + +## Runtime consumption today (left untouched this iteration) + +- `api/oss/src/core/secrets/utils.py:54` `get_user_llm_providers_secrets` filters to + `kind == "provider_key"` and maps each to a fixed env var name. New kind is ignored here + by construction, which is what we want for now. + +## Frontend: the secret entity package + Settings tables + +- Entry: Settings -> "Providers & Models" renders + `web/oss/src/components/pages/settings/Secrets/Secrets.tsx`, which today mounts two + ``. +- Table: `web/oss/src/components/pages/settings/Secrets/SecretProviderTable/index.tsx` — + heavily provider-specific (LLM icons, provider tag, model tags). Reusing it for a plain + secret would mean threading a third `type` through many branches. A small dedicated + table + modal is cleaner. +- Data layer (entity package) `web/packages/agenta-entities/src/secret/`: + - `core/types.ts` — re-exports Fern types. `SecretKind` (`:42`) and the DTO aliases come + from `@agentaai/api-client`. After backend regen, `SecretKind.CustomSecret` and a + `CustomSecretDto` alias appear here. + - `core/transforms.ts` — `transformSecret` (`:68`) maps each wire kind into the common + `LlmProvider` row. `transformCustomProviderPayloadData` (`:120`) builds the + `CreateSecretDto`. We add a `custom_secret` branch and a `transformCustomSecretPayloadData`. + - `state/atoms.ts` — query atom (`:78`), `customSecretsAtom` (`:130`, filters by kind), + mutation atoms (`:144`), and per-kind create action atoms (`createCustomSecretAtom` + `:229`, `deleteSecretAtom` `:259`). We add a `customNamedSecretsAtom` + + `createCustomNamedSecretAtom` following the custom-provider pattern; delete is reused. + - `state/useVaultSecret.ts` — the hook (`:50`) exposes `secrets`, `customRowSecrets`, + and `handleModify*` callbacks. We add `namedSecrets` + `handleModifyNamedSecret`; + `handleDeleteVaultSecret` is reused as-is. + - `api/api.ts` — `fetchVaultSecret` runs `transformSecret`; create/update/delete are + generic over `CreateSecretDto`. No change needed. +- Shared row type: `web/packages/agenta-shared/src/types/llmProvider.ts:13` `LlmProvider` + already has `name`, `key`, `id`, `type`, `created_at`. Reuse it for named-secret rows. +- Fern regen: per `web/CLAUDE.md`, after backend OpenAPI changes run + `bash ./clients/scripts/generate.sh --language typescript` then `pnpm install` (rebuilds + `@agentaai/api-client` `dist/`) so the new enum value + DTO reach the frontend. + +## Existing tests to mirror + +- `api/oss/tests/legacy/vault_router/test_vault_secrets_apis.py` exercises the vault CRUD. + Add a `custom_secret` case here (create -> list -> get -> update -> delete, value masked + on the wire and decrypted round-trips). diff --git a/docs/design/vault-named-secrets/status.md b/docs/design/vault-named-secrets/status.md new file mode 100644 index 0000000000..98ec892654 --- /dev/null +++ b/docs/design/vault-named-secrets/status.md @@ -0,0 +1,48 @@ +# Status + +Source of truth for progress. Update as work lands. + +_Last updated: 2026-06-18 (planning complete, implementation not started)._ + +## Current state + +Planning only. No code changed. Research verified against the repo. + +## Checklist + +### Phase 1 — Backend +- [ ] Add `SecretKind.CUSTOM_SECRET` (`api/oss/src/core/secrets/enums.py`) +- [ ] Add `CustomSecretDTO` + union member + validator branch (`.../secrets/dtos.py`) +- [ ] `ruff format` + `ruff check --fix` in `api/` + +### Phase 2 — Migration +- [ ] Re-confirm current core head (compute by elimination) +- [ ] Add `ALTER TYPE secretkind_enum ADD VALUE 'CUSTOM_SECRET'` revision to OSS tree +- [ ] Add the identical revision to EE tree (same `revision` id) +- [ ] `alembic upgrade head` against the dev DB + +### Phase 3 — Fern client +- [ ] `bash ./clients/scripts/generate.sh --language typescript` +- [ ] `pnpm install` and confirm `SecretKind.CustomSecret` + `CustomSecretDto` exist + +### Phase 4 — Frontend +- [ ] `CustomSecretDto` alias in `core/types.ts` +- [ ] `transformSecret` branch + `transformCustomSecretPayloadData` in `core/transforms.ts` +- [ ] `customNamedSecretsAtom` + `createCustomNamedSecretAtom` in `state/atoms.ts` +- [ ] `namedSecrets` + `handleModifyNamedSecret` in `state/useVaultSecret.ts` +- [ ] `NamedSecretTable` + `ConfigureSecretModal` under `settings/Secrets/` +- [ ] Mount `` in `Secrets.tsx` +- [ ] `pnpm lint-fix` in `web/` + +### Verification +- [ ] Backend: extend `api/oss/tests/legacy/vault_router/test_vault_secrets_apis.py` with a + `custom_secret` create/list/get/update/delete case; value decrypts round-trip +- [ ] Manual API: `POST /vault/v1/secrets/` with + `{header:{name:"GITHUB_TOKEN"}, secret:{kind:"custom_secret", data:{secret:{key:"x"}}}}` +- [ ] UI: Settings -> Providers & Models -> add a secret, reload, edit, delete + +## Open questions / decisions log + +- 2026-06-18: Storage scope confirmed as **project vault** (not per-agent). New + `SecretKind = custom_secret`. Data shape `{secret:{key}}`, name in `header.name`. +- 2026-06-18: Runtime/agent wiring is **out of scope** for this iteration. diff --git a/hosting/docker-compose/ee/docker-compose.dev.yml b/hosting/docker-compose/ee/docker-compose.dev.yml index b1f9a2d773..286d0d6aa7 100644 --- a/hosting/docker-compose/ee/docker-compose.dev.yml +++ b/hosting/docker-compose/ee/docker-compose.dev.yml @@ -394,7 +394,7 @@ services: - ${ENV_FILE:-./.env.ee.dev} environment: DOCKER_NETWORK_MODE: ${DOCKER_NETWORK_MODE:-bridge} - # Agent workflow (WP-2): reach the harness wrapper sidecar in-network. + # Agent workflow: reach the agent runner sidecar in-network. AGENTA_AGENT_PI_URL: http://agent-pi:8765 # Agent runtime (WP-8): drive the harness over ACP via a rivet daemon. # Harness (pi/claude) and sandbox (local/daytona) are independent axes. @@ -420,7 +420,7 @@ services: agent-pi: # === IMAGE ================================================ # - # Pi harness sidecar for the agent workflow (WP-2). The services container + # Agent runner sidecar. The services container # calls it in-network at http://agent-pi:8765 (AGENTA_AGENT_PI_URL). build: context: ../../../services/agent @@ -450,8 +450,11 @@ services: DAYTONA_API_KEY: ${DAYTONA_API_KEY:-} DAYTONA_API_URL: ${DAYTONA_API_URL:-} DAYTONA_TARGET: ${DAYTONA_TARGET:-} - # Pre-baked snapshot (rivet daemon + Pi + Claude + certs) so Daytona runs skip - # the ~150s per-invoke `npm install pi`. Built by poc/build_rivet_snapshot.py. + # Pre-baked snapshot (rivet daemon + Pi + certs; Claude inherited from rivet's + # -full base) so Daytona runs skip the ~150s per-invoke `npm install pi`. We + # ship the builder (poc/build_rivet_snapshot.py), not the snapshot itself: each + # operator builds their own, so we never distribute a Claude-containing image. + # See services/agent/docker/README.md for the licensing posture. AGENTA_RIVET_DAYTONA_SNAPSHOT: ${AGENTA_RIVET_DAYTONA_SNAPSHOT:-agenta-rivet-pi} AGENTA_RIVET_DAYTONA_INSTALL_PI: ${AGENTA_RIVET_DAYTONA_INSTALL_PI:-false} # === STORAGE ============================================== # diff --git a/sdks/python/agenta/sdk/agents/adapters/harnesses.py b/sdks/python/agenta/sdk/agents/adapters/harnesses.py index 0ac53076a8..e718c1db2b 100644 --- a/sdks/python/agenta/sdk/agents/adapters/harnesses.py +++ b/sdks/python/agenta/sdk/agents/adapters/harnesses.py @@ -48,7 +48,7 @@ def _opt_str(value: Any) -> Any: def _normalize_tool_specs(specs: List[Dict[str, Any]]) -> List[ToolSpec]: - """Compatibility helper for callers still supplying runner dictionaries.""" + """Compatibility helper for old tests/callers still supplying runner dictionaries.""" return [coerce_tool_spec(spec) for spec in specs or []] diff --git a/services/agent/README.md b/services/agent/README.md index 4a96328479..82b5272e17 100644 --- a/services/agent/README.md +++ b/services/agent/README.md @@ -11,11 +11,12 @@ rivet's `sandbox-agent`) are Node libraries with no Python SDK. Two entrypoints, same `/run` contract (see `src/protocol.ts`): - **`src/cli.ts`** — one JSON request on stdin, one result on stdout. The Python - `SubprocessHarness` spawns this for local runs. stdout is the result channel only; logs - go to stderr. + SDK adapters use this subprocess transport when `AGENTA_AGENT_PI_URL` is unset. stdout is + the result channel only; logs go to stderr. - **`src/server.ts`** — the same thing as a long-lived HTTP server on `:8765` - (`GET /health`, `POST /run`). This is the **dockerized sidecar** the Python `HttpHarness` - calls in-network. The dev image (`docker/Dockerfile.dev`) runs `tsx watch src/server.ts`. + (`GET /health`, `POST /run`). This is the dockerized agent runner sidecar the Python SDK + adapters call over HTTP when `AGENTA_AGENT_PI_URL` points at it. The dev image + (`docker/Dockerfile.dev`) runs `tsx watch src/server.ts`. Both route to an engine by the request's `backend` field. @@ -27,12 +28,14 @@ src/ server.ts entrypoint: HTTP sidecar on :8765 protocol.ts the /run wire contract (request, result, events, capabilities) engines/ - pi.ts legacy engine: drive the Pi SDK in-process + pi.ts engine: drive the Pi SDK in-process rivet.ts engine: drive a harness over ACP via a rivet sandbox-agent daemon tracing/ otel.ts turn a run into OpenTelemetry spans nested under /invoke tools/ - client.ts the one /tools/call HTTP client + callback.ts the one /tools/call HTTP client + code.ts execute resolved code tools in a scoped subprocess + dispatch.ts dispatch resolved tools by executor kind mcp-bridge.ts build the MCP server config that exposes tools to a harness mcp-server.ts the stdio MCP server itself (launched per session by the daemon) extensions/ @@ -41,14 +44,14 @@ src/ ## Engines -- **`pi`** (`engines/pi.ts`) — the legacy path. Drives the Pi SDK directly in-process. +- **`pi`** (`engines/pi.ts`) — drives the Pi SDK directly in-process. - **`rivet`** (`engines/rivet.ts`) — drives any harness (`pi`, `claude`) over the Agent Client Protocol through a rivet `sandbox-agent` daemon, either local or in a Daytona sandbox. This is the default on the platform. The engine is a deployment choice (`backend` on the wire / `AGENT_BACKEND` env), not a -harness. Harness choice (pi/claude) and sandbox (local/daytona) are per-run config the -Python service sends. +harness. Harness choice (`pi`, `claude`, or experimental `agenta`) and sandbox (`local` or +`daytona`, where supported) are per-run config the Python service sends. ## Result diff --git a/services/oss/src/agent/app.py b/services/oss/src/agent/app.py index 33d1986b59..ac1bdbcc1b 100644 --- a/services/oss/src/agent/app.py +++ b/services/oss/src/agent/app.py @@ -106,9 +106,9 @@ async def _agent( mcp_servers=resources.mcp_servers, ) - # The harness validates that the chosen backend can drive it; select_backend already - # routes a claude harness or a non-local sandbox to rivet, so this never fails in - # practice. setup/cleanup own the backend lifecycle; prompt/stream run one cold turn. + # The harness validates that the chosen backend can drive it. Unsupported combinations + # such as `agenta` on rivet fail here instead of silently changing runtime behavior. + # setup/cleanup own the backend lifecycle; prompt/stream run one cold turn. harness = make_harness(selection.harness, Environment(select_backend(selection))) # The `/messages` SSE path sets `stream`: return the Vercel UI Message Stream as an async @@ -150,7 +150,7 @@ async def _agent_vercel_stream(harness, session_config, msgs): def create_agent_app(): app = ag.create_app() # No builtin URI yet: registering the agent as a first-class workflow type - # (`agenta:builtin:agent:v0`) and its interface is WP-6. Here we register the handler + # (`agenta:builtin:agent:v0`) is still future work. Here we register the handler # directly, so it gets an auto URI (`user:custom:...`) and runs locally. routed = ag.workflow(schemas=AGENT_SCHEMAS)(_agent) # is_agent gates the agent-only `/messages` + `/load-session` routes (next to /invoke). diff --git a/services/oss/src/agent/schemas.py b/services/oss/src/agent/schemas.py index e696e84500..7047734a01 100644 --- a/services/oss/src/agent/schemas.py +++ b/services/oss/src/agent/schemas.py @@ -39,8 +39,9 @@ # the `agent_config` catalog type (AgentConfigSchema in agenta.sdk.utils.types), so this is a # thin `x-ag-type-ref` the playground resolves against `/workflows/catalog/types/agent_config` # and dispatches to the AgentConfigControl (web/packages/agenta-entity-ui/.../AgentConfigControl.tsx). -# Generating the schema from the model keeps the typed tools/mcp_servers in one place; we only -# carry the default here (what the playground pre-fills). agent.py reads this value (see inputs.py). +# The catalog type keeps the typed tools/mcp_servers shape in one place; this schema only +# carries the default that the playground pre-fills. The agent handler reads it from +# `parameters.agent` in app.py. _DEFAULT_AGENT_CONFIG = { "agents_md": _DEFAULT_AGENTS_MD, "model": _DEFAULT_MODEL,