Durable agent systems for the JVM.
Cortavyn is a Java 25, Maven-based foundation for durable agent runs. Core orchestration, graph execution, chat, deep agents, and model-provider adapters are separate artifacts so applications only depend on the capabilities they use.
All production packages are @NullMarked with JSpecify 1.0.0: reference types are non-null by default, and optional values are explicitly annotated with @Nullable.
mvn verify enforces these contracts with NullAway in JSpecify mode; violations and missing explicit package marking fail the build.
| Artifact | Purpose |
|---|---|
cortavyn-core |
Durable run and state contracts |
cortavyn-model-api |
Portable chat-model contracts |
cortavyn-graph |
Stateful graph runtime, checkpoints, routing, and streaming |
cortavyn-chat |
Conversation and chat-session contracts |
cortavyn-deep |
Deep-agent harness, isolated workspace, planning, and extension contracts |
cortavyn-provider-* |
Optional provider integration boundaries |
graph, chat, and provider modules depend only on the APIs they need. deep composes graph and model-api; no core module depends on a provider SDK.
ROADMAP.md tracks the remaining chat-model capabilities and their implementation status.
The versioned Structurizr C4 model documents module responsibilities and dependency directions. The cortavyn-architecture module enforces those directions with ArchUnit as part of mvn test.
The agent and tool flow explains the execution loop and runtime ownership with Mermaid diagrams.
ChatTool.typed derives a provider JSON Schema from a Java record and converts a tool call back to that type. @ToolName and @ToolDescription are optional convenience metadata; the explicit overload accepts the name and description directly. @Nullable record components are not required by the generated schema.
@ToolName("get_weather")
@ToolDescription("Gets the current weather for a city.")
record WeatherArguments(@ToolDescription("The city to look up.") String city) { }
var weather = ChatTool.typed(WeatherArguments.class, arguments ->
CompletableFuture.completedFuture(ToolExecutionResult.success(weatherFor(arguments.city()))));Runtime-aware tools additionally receive ToolRuntime with application context, an injected ToolStore, and a ToolProgressWriter. Configure it once on ChatAgent.builder(model).runtime(runtime).
cortavyn-graph is a provider-independent, LangGraph-inspired orchestration runtime. Define a StateSchema, register asynchronous nodes, connect START and END, then compile the immutable graph. Nodes return a partial StateUpdate; channels decide whether a value is replaced, reduced, collected as a topic, or cleared after a superstep.
var schema = StateSchema.builder(GraphState.adapter())
.channel("steps", StateChannel.topic())
.channel("answer", StateChannel.lastValue())
.build();
var graph = new StateGraph<>(schema)
.addNode("research", (state, runtime) -> completedFuture(new StateUpdate(Map.of("steps", "research"))))
.addNode("answer", (state, runtime) -> completedFuture(new StateUpdate(Map.of("answer", "done"))))
.addEdge(StateGraph.START, "research")
.addEdge("research", "answer")
.addEdge("answer", StateGraph.END)
.compile();
RunResult<GraphState> result = graph.invoke("customer-42", GraphState.empty()).toCompletableFuture().join();Command combines an update with an explicit route, while Send dynamically fans out work. The runtime executes each superstep concurrently up to its configured limit and merges updates in stable node order. CheckpointStore persists a snapshot after every superstep; InMemoryCheckpointStore is the reference implementation. Production stores receive a StateCodec from the application rather than relying on implicit object serialization.
Use interruptBefore, interruptAfter, or an Interrupt result to pause a graph. The returned ResumeToken resumes the same thread with a caller-supplied StateUpdate; fork(checkpointId) starts a new run from history. stream exposes state, update, retry, checkpoint, interrupt, debug, and custom node events through Flow.Publisher. CompiledGraph.toMermaid() exports its topology.
cortavyn-chat provides ChatAgentNode as the optional bridge: applications create a run-scoped ChatSession from NodeRuntime, making the graph run ID available when constructing ToolRuntime. Use GraphToolProgressWriter in that runtime to expose tool progress as graph custom events.
DeepAgent provides a portable model/tool loop with a thread-isolated virtual workspace and built-in filesystem and todo tools. The default in-memory workspace accepts safe relative paths only; applications can provide another DeepWorkspace implementation for durable or host-backed storage.
var agent = DeepAgent.builder(model)
.systemPrompt("You are a careful research assistant.")
.workspace(new InMemoryWorkspace())
.build();
DeepRun run = agent.invoke("research-42", "Collect findings in notes/findings.txt")
.toCompletableFuture().join();The harness exposes ls, read_file, write_file, edit_file, glob, grep, write_todos, persistent-memory and skill tools. write_file, edit_file, write_memory, and an optional execute tool pause by default; resume with one ApprovalDecision per pending action. The default virtual workspace implements CheckpointableWorkspace, so its file snapshot travels in DeepPendingRun; applications choose durable stores for paused runs, todos, and asynchronous subagent tasks through the corresponding Deep*Store interfaces.
invoke and resume execute through an internally compiled StateGraph; a pending approval therefore becomes a durable graph interrupt. stream(DeepRequest) publishes typed model, tool, todo, context-offload, subagent and approval events before its terminal event. DeepAgentNode additionally adapts the harness into application-defined graphs; a List<ApprovalDecision> in its configured state key resumes the same deep run.
The provider-free cortavyn-example-deep demonstrates the virtual workspace, a bundled write approval and a registered specialist without requiring credentials:
mvn -pl :cortavyn-example-deep -am package -Prun-exampleUse PermissionedWorkspace for first-match-wins read/write path rules and FilesystemWorkspace only with a deliberately selected root. ProcessSandbox is an explicit local-development backend, not an untrusted-code isolation boundary; production applications should provide an isolated Sandbox implementation. MCP applications pass tools and resource workspaces through mcpSources(...); agents can read resources through read_mcp_resource.
Request a Java record directly from any chat model. Cortavyn derives JSON Schema from nested records, lists, maps and enums, rejects unknown or mistyped fields locally, and keeps the original provider response available for usage and request metadata.
record Weather(String city, int temperature) { }
Weather answer = model.withStructuredOutput(Weather.class)
.complete(new ChatRequest(List.of(new ChatMessage(ChatMessageRole.USER, "Weather in Berlin?"))))
.toCompletableFuture().join().value();OpenAI and Azure use strict JSON Schema response formats; Gemini uses responseMimeType and responseJsonSchema; Anthropic uses a forced schema tool. Mistral and OpenAI-compatible adapters request their JSON Schema response format. Other models receive a synthetic tool automatically, and plain JSON text remains supported as a fallback. Invalid responses throw StructuredOutputException, whose violations() identifies every invalid field.
Requires Java 25 and Maven 3.9+.
mvn verifyLibrary artifacts are released to Maven Central from an annotated version tag such as v0.1.0.
The GitHub Actions environment maven-central must provide these secrets:
CENTRAL_TOKEN_USERNAMEandCENTRAL_TOKEN_PASSWORD: a user token from the Central PortalMAVEN_GPG_PRIVATE_KEY: the armored private signing keyMAVEN_GPG_PASSPHRASE: its passphrase
Before the first release, claim and verify the io.cortavyn namespace in the Central Portal and publish the GPG public key to a supported key server. The workflow builds, signs, and uploads the library modules; example modules are deliberately excluded. It leaves a validated deployment in the Central Portal for an explicit final Publish action, because Central artifacts cannot be modified or removed once public.
The examples are executable smoke-test applications. They are compiled by the normal build but never call a provider automatically. Every chat-provider example now runs a short two-turn research conversation: it creates an initial answer and then asks the same model to critically review and improve it. Every graph-provider example runs a durable four-step workflow (plan, research, review, synthesis), prints its checkpoint count, and renders the resulting graph as Mermaid.
OPENAI_API_KEY=... mvn -pl :cortavyn-example-openai-chat -am package -Prun-example
OPENAI_API_KEY=... mvn -pl :cortavyn-example-openai-graph -am package -Prun-example
OPENAI_API_KEY=... mvn -pl :cortavyn-example-openai-deep-agent -am package -Prun-example
OPENAI_API_KEY=... mvn -pl :cortavyn-example-openai-tool-agent -am package -Prun-example
MISTRAL_API_KEY=... mvn -pl :cortavyn-example-mistral-chat -am package -Prun-example
MISTRAL_API_KEY=... mvn -pl :cortavyn-example-mistral-tool-agent -am package -Prun-example
MISTRAL_API_KEY=... mvn -pl :cortavyn-example-mistral-structured-output -am package -Prun-example
MISTRAL_API_KEY=... mvn -pl :cortavyn-example-mistral-operations -am package -Prun-example
MISTRAL_API_KEY=... mvn -pl :cortavyn-example-mistral-deep-agent -am package -Prun-example
GEMINI_API_KEY=... mvn -pl :cortavyn-example-gemini-chat -am package -Prun-example
GEMINI_API_KEY=... mvn -pl :cortavyn-example-gemini-deep-agent -am package -Prun-example
OPENROUTER_API_KEY=... mvn -pl :cortavyn-example-openrouter-chat -am package -Prun-example
OPENROUTER_API_KEY=... mvn -pl :cortavyn-example-openrouter-deep-agent -am package -Prun-example
ANTHROPIC_API_KEY=... mvn -pl :cortavyn-example-anthropic-chat -am package -Prun-example
ANTHROPIC_API_KEY=... mvn -pl :cortavyn-example-anthropic-deep-agent -am package -Prun-example
mvn -pl :cortavyn-example-ollama-chat -am package -Prun-example
mvn -pl :cortavyn-example-ollama-deep-agent -am package -Prun-example
AZURE_OPENAI_ENDPOINT=... AZURE_OPENAI_API_KEY=... AZURE_OPENAI_DEPLOYMENT=... AZURE_OPENAI_API_VERSION=... mvn -pl :cortavyn-example-azure-openai-chat -am package -Prun-example
AZURE_OPENAI_ENDPOINT=... AZURE_OPENAI_API_KEY=... AZURE_OPENAI_DEPLOYMENT=... AZURE_OPENAI_API_VERSION=... mvn -pl :cortavyn-example-azure-openai-deep-agent -am package -Prun-example
AWS_BEDROCK_MODEL=... mvn -pl :cortavyn-example-aws-bedrock-chat -am package -Prun-example
AWS_BEDROCK_MODEL=... mvn -pl :cortavyn-example-aws-bedrock-deep-agent -am package -Prun-examplePass a prompt as Maven property with -Dexample.prompt="Explain durable agents in one sentence.". Every *-deep-agent example uses provider tool calling for a todo plan, an isolated review specialist, a virtual workspace write, and a bundled human approval followed by resume. Mistral examples are grouped under examples/mistral; the operations example demonstrates the profile registry, factory, cache, retry/backoff, bounded concurrency, and metrics. The OpenAI example also accepts OPENAI_MODEL; Mistral uses the provider default unless MISTRAL_MODEL is set. The Gemini example follows LangChain's environment convention: GOOGLE_API_KEY takes precedence over GEMINI_API_KEY, and GEMINI_MODEL overrides its gemini-2.5-flash default.
OpenRouter uses OPENROUTER_MODEL to choose a catalog model, plus optional OPENROUTER_SITE_URL and OPENROUTER_APP_TITLE for application attribution.
Azure OpenAI requires its resource endpoint, API key, deployment name, and API version.
Bedrock uses the AWS SDK default credential and region provider chains; set AWS_BEDROCK_MODEL to a model ID available in the selected region.
Anthropic accepts ANTHROPIC_MODEL.
The local Ollama example defaults to http://localhost:11434 and llama3.2; override them with OLLAMA_BASE_URL and OLLAMA_MODEL.
Apache-2.0. See LICENSE.