Repository navigation
DAGOS 0→1: core runtime, context management, tools, and semantic review - #22
Conversation
|
Important
This repository does not receive automatic reviews because it has fewer than 10 stars. ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Advanced Run ID: Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
- Cargo workspace (edition 2024): `dagos-core` library and `dagos` CLI. - dagos-core declares the architecture layers (domain, contracts, store, context, ir, provider, response, runtime) with documented dependency rules. - tests/boundaries.rs enforces the rules: providers cannot reference the store, the domain depends on nothing, and dagos-core cannot depend on HTTP stacks or provider SDKs. - CI runs fmt, clippy (-D warnings), and tests on Ubuntu and Windows. Closes #2 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Migration 0001 creates projects, dag_nodes, dag_edges, runs, events, and active_context as STRICT tables. - Constraints protect the core invariants at the database level: typed node/edge enums, JSON-object payloads, same-project edges (composite FKs), no self-loops or duplicate edges, run status/completion consistency, one running run per project, unique positive event sequences, and active-context rows that only reference nodes. - Triggers make DAG nodes/edges durable (no delete) and events append-only. - Migrations are tracked in PRAGMA user_version and applied one transaction per step; failed migrations roll back and newer databases are rejected. - Store opens file or in-memory databases with foreign keys enabled. - data-model.md records the project scoping and schema-enforced rules. Closes #3 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Prefixed, validated ID newtypes (proj_/node_/edge_/run_/evt_) with an injectable IdGenerator: UUIDv7 in production, per-prefix sequential IDs for deterministic tests. - Timestamp: UTC millisecond instants in one fixed-width RFC 3339 form that sorts chronologically; strict parsing rejects non-canonical and impossible dates. Injectable Clock (system / stepping). - Closed enums with fixed wire names for node types, edge types, run status, error codes, classifications, and context sources; unknown values are rejected by FromStr and serde alike. - DagNode, DagEdge, Run, RunConfig, ContextMember, Project, and typed Event data split into stored (type, payload) pairs. - Deterministic JSON: declaration-ordered fields and sorted payload keys (serde_json preserve_order is not enabled anywhere in the tree). - contracts module embeds the Spec Kit JSON Schemas verbatim, compiles them once, and reports every violation with its JSON Pointer. - dag.schema.json finalized to match the data model: node ID and timestamp patterns, required timestamps. - tests/contracts.rs keeps the domain types and published schemas in agreement. Closes #4 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Store::transaction runs work in one IMMEDIATE SQLite transaction that commits on Ok and rolls back on Err; repository methods live on Tx. - The store owns the injected Clock and IdGenerator, so every record gets its ID and timestamps from one place (deterministic in tests). - Projects: create, get, list. - Nodes: insert, get, list in creation order, payload update (advances updated_at only). Nodes are never deleted. - Edges: insert with typed DagViolation errors for unknown/cross-project endpoints, self-loops, duplicates, and cycles (recursive reachability check keeps the graph acyclic); get, list in creation order. - Column conversions re-validate domain values on read. - Integration tests cover round trips, project scoping, validation, transactional rollback, and persistence across restart. - rustfmt uses max small heuristics for compact, readable code. Closes #5 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
🔍 1 thing to check
✅ Health of changed files: 9.3 📊 See the full report for this PR |
- Runs: create (records run.started as event 1 with provider, model, and system prompt), get, list, previous run, running run(s), complete and fail. Terminal events are written in the same transaction as the status change; only running runs change state; one running run per project (explicit RunInProgress error). - Events: append with store-assigned sequences (1, 2, 3, ... per run), only while the run is running; read all or after a sequence cursor. Stored payloads are re-validated into typed EventData on read. - Active context: replace_context writes a running run's whole membership atomically, ordered by node creation (created_at, id) with its source (carried/jev); members must be unique nodes of the run's project. Leaving a node out removes only its membership row, never the node. A finished run's context is frozen history. - Integration tests cover identity retention, per-run monotonic sequences, lifecycle freezing, context ordering/isolation/removal, and restart persistence including crashed-run discovery. Closes #6 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- kiss.jev-context.v1 finalized: output is a list of {node_id,
classification} with a closed label set (active | inactive) and no
additional properties anywhere, so plans, actions, tool calls, provider
routing, reasons, and prose have nowhere to go.
- New kiss.jev-request.v1: the run's message, every candidate node (oldest
first, with whether it is in the carried-over context), and the edges
between candidates.
- Domain mirror: JevRequest, JevCandidate, JevEdge, ContextClassification,
NodeClassification, and zero-sized schema version tags that refuse any
other document kind or version.
- Contract::parse fails closed for untrusted text: JSON, then schema, then
the typed mirror.
- tests/jev_contract.rs rejects agent-shaped output (plan, actions,
tool_calls, provider, route, reasoning, prose, extra per-entry data,
unknown labels, wrong versions, non-JSON) at both the schema and the
typed mirror.
Closes #7
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- JevClassifier: the runtime depends on this async adapter, not a model API. Adapters receive a kiss.jev-request.v1 request and return raw kiss.jev-context.v1 text; DAGOS validates it, so adapters cannot bypass the contract. JevError distinguishes "no output" from invalid output. - FakeJev: offline and deterministic. Default policy: superseded nodes are inactive, conversation turns beyond the most recent window (default 6) are inactive, everything else is active. Scripted and unavailable modes exercise fail-closed handling. - Tests: contract-conforming output, the policy rules, and byte-identical replay of a request recorded as JSON. Closes #8 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- carry_context seeds a new run's context with the previous run's members (source carried) and records context.carried (from_run_id null for a project's first run), so context persists across turns until Jev changes it. - classification_request builds kiss.jev-request.v1 from store state: every project node except the run's own message (oldest first, flagged with current membership) and the edges between candidates. - validate_classification fails closed: contract violations, nodes that were not candidates (including the run's task node), and duplicate classifications are rejected before anything is applied. It is pure, so the runtime can commit a jev.rejected record for rejected output. - apply_classification: active -> member (source jev), inactive -> not a member, unclassified -> unchanged. Records jev.classified then one context.added/context.removed per change. Only membership changes; the durable DAG is untouched. - New events: context.carried, jev.requested, jev.classified, jev.rejected, context.added, context.removed. - Tests cover carrying, request shape, membership-only updates with event history, fail-closed validation, and replay of a recorded classification with the fake Jev. Closes #9 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- kiss.inference-ir.v1 finalized as the only provider-facing contract:
system_prompt, task {node_id, message}, context items {node_id, type,
payload, relations} in stable order, recent_events (how the project's
latest earlier runs ended, with failure code and message), and optional
descriptive tools {name, description, input_schema}. No storage fields
(timestamps, project scope, payload_json) exist anywhere in it.
- Domain mirror: InferenceIr, IrTask, IrContextItem, IrRelation, IrEvent,
IrEventType, IrTool, and the kiss.inference-ir.v1 schema tag. IR types are
distinct from DAG records, so a provider accepting InferenceIr cannot be
handed a raw DAG record.
- tests/ir_contract.rs: valid IR validates and round-trips; serialization
is byte-deterministic; storage fields, a raw DagNode as a context item,
and fields outside the contract are all rejected.
Closes #10
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- ir::compile reads a run's state and returns fresh InferenceIr: the
run's system prompt, the task from its user conversation node, the
active context in stored (node creation) order, each item's outgoing
relations to other IR nodes, how the project's 3 most recent earlier
runs ended (failures carry code and message), and caller-supplied
capability descriptions. The result is validated against
kiss.inference-ir.v1 before it is returned.
- Compilation only reads; every value is projected into IR types, so no
storage record reaches a provider.
- ConversationTurn fixes the {role, text} payload of conversation nodes
shared by the runtime and the compiler.
- Snapshot tests (tests/snapshots, UPDATE_SNAPSHOTS=1 to rewrite) cover a
first run and a project with superseded/dependent decisions, a
completed run, a failed run, and tools. Further tests: identical state
(including an independently rebuilt store) compiles byte-identically,
explicit system prompt, stable creation-order context, no storage keys,
and rejection of a task that is not a user turn.
- Store transactions now panic with an explanation when nested on one
thread instead of deadlocking on the store mutex.
Closes #11
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- InferenceProvider: the interface every inference backend implements.
The entire boundary is InferenceRequest { model_id, ir: &InferenceIr },
so an adapter can only ever receive compiled, versioned IR.
- Providers stream presentation-prose deltas through a DeltaSink and
return their raw final output; DAGOS validates it before use, so
adapters cannot bypass the response contract.
- Provider identity (ProviderId) and the model (ModelId, opaque to the
core, interpreted only by the adapter) are explicit.
- ProviderError separates "no output" (unknown model, request failure)
from invalid output, which validation catches later.
- tests/provider_boundary.rs: a spy adapter behind Arc<dyn
InferenceProvider> receives exactly the compiled IR (validated against
kiss.inference-ir.v1) and the model ID, nothing else.
Closes #12
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- kiss.inference-response.v1 finalized with its Rust mirror: presentation.prose (for people, never canonical), emissions (node emissions with a response-local ref that can never look like a node ID; edge emissions whose endpoints are refs or node IDs), and optional tool_calls and metadata that are recorded but never executed or interpreted. - FakeProvider: offline and deterministic; output is a function of the IR and model ID alone. Prose streams word by word (optional per-delta delay for people watching). The model ID selects behaviour: fake-echo (valid: prose + an observation observed_from the task), fake-malformed, fake-invalid-schema, fake-dangling-edge, fake-cycle, fake-timeout (never finishes), fake-error. - Tests: deltas reassemble the final prose, output and streaming order are deterministic (also under paced, paused-clock streaming), every failure mode is reproducible, identity and models are explicit, and DAG state outside the IR cannot influence the provider. Closes #13 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- validate_response parses raw provider output fail-closed: it must be JSON, satisfy kiss.inference-response.v1, declare each emission ref once, and connect edges only to refs declared in the response or to nodes that were present in the IR (a model cannot reference nodes it was never shown). - ValidatedResponse can only be constructed by validation; it is the form in which emissions will be applied, so only validated final emissions can mutate the DAG. Live-state DAG rules (duplicates, cycles) are left to atomic application. - Tests: separation of prose and emissions with tool_calls/metadata preserved, malformed JSON, schema violations rejected by contract and typed mirror alike, duplicate refs, unknown/unseen endpoints, and streamed prose never making invalid output usable. - research.md records that assistant prose never becomes DAG state: it persists in the event history, and models persist what they need through emissions. Closes #14 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Runtime::run executes the whole pipeline: record the run and the user message (a conversation node, the IR task), carry context, ask Jev and apply valid classifications, compile and record IR, call the selected provider under a deadline while persisting each streamed delta, record the raw output, validate it, then atomically record response.validated, create the emitted nodes and edges, and complete the run. - Pipeline failures are run state, not Rust errors: jev_failed, jev_invalid_output (with the rejected output kept in jev.rejected), ir_invalid, provider_failed, provider_timeout, response_invalid and emission_rejected (with response.rejected evidence) all finish the run as failed with everything recorded before the failure intact. A DAG violation while applying emissions rolls back every emission. - Runtime errors are reserved for runs that cannot start (unknown provider, run in progress, store failure); recover_interrupted_runs fails runs a crashed process left running. - New events: message.recorded, ir.compiled, inference.started, inference.delta, inference.completed, response.validated, response.rejected, dag.node_created, dag.edge_created. - IR recent_events now carries a completed run's presented prose, from the event history (never DAG state), so conversations keep their thread. - tests/runtime_fake_run.rs: complete offline run with the exact event timeline, contiguous sequences, durable message/observation/edge, IR recorded equal to recompiled IR, streamed prose equal to validated prose; context and reply carried into the next run; every provider/response failure mode explicit with a clean DAG; Jev failures stop before inference; unstartable runs create nothing; crash recovery; Send futures. Closes #15 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- New crate dagos-openai: one adapter for any streaming Chat Completions endpoint (OpenRouter, OpenAI, Z.ai, Ollama, LM Studio, vLLM). Authentication, HTTP, SSE streaming, and request formatting stay inside it; dagos-core is unchanged and still has no HTTP dependency. - Request: the system message is the IR's editable system prompt followed by the fixed DAGOS protocol instructions and the response schema; the user message is the compiled IR itself as compact JSON. Optional response_format json_object. API keys are config-only and redacted from Debug output. - Streaming: an SSE decoder safe across chunk and UTF-8 boundaries, and an incremental JSON scanner that streams only presentation.prose as deltas (escapes and surrogate pairs decoded, "prose" keys elsewhere ignored). The raw document is returned verbatim for DAGOS to validate. - Errors (HTTP status, error events in the stream, unreachable endpoints) map to ProviderError::Failed and so to run.failed with provider_failed; non-JSON model output becomes response_invalid. - Tests against a local mock endpoint: request shape (path, bearer auth, model, stream, json mode, IR as the user message), prose streaming, error mapping, and full DAGOS runs through the adapter. An #[ignore] live test runs against a real endpoint when DAGOS_LIVE_* is set. Closes #16 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The earlier escape test's fixture held literal UTF-8 instead of JSON \uXXXX sequences, so unicode-escape and surrogate-pair decoding were not actually covered. The new test builds the escapes from parts, asserts the fixture really contains them, and checks decoding across every small chunk size, plus a lone high surrogate. The old test is renamed to what it covers: simple escapes and literal UTF-8. Refs #16 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Per-project run defaults (provider, model, editable system prompt) are persisted in SQLite (migration 0002, the first real upgrade step) and drive subsequent runs; each run still records the configuration it ran with, so edits never rewrite history. Runtime::set_defaults rejects unregistered providers; model IDs stay opaque to the core. - Store::with_event_listener observes committed events only, in order, after the store is released; rolled-back events are never seen. - The dagos CLI (transport) gains init, config, run, and recover. `run` streams presentation prose to stdout via the listener, reports explicit failures with their error code, exits 1 for failed runs, and supports --json; per-run --provider/--model/--system-prompt overrides leave the defaults alone; --inference-timeout bounds provider calls. - Provider connection configuration stays outside core domain types: the binary always offers the fake provider, enables openrouter/openai when their standard key variables are set, and reads extra OpenAI-compatible endpoints from .dagos/providers.json. Keys come only from environment variables and are never stored. - Tests: defaults (validation, per-project, restart), edited prompts only affecting later runs, identical IR and DAG when only provider/model change, committed-only event observation, provider configuration loading, and the CLI end to end (streaming, failures, persistence, missing workspace, crash recovery). Closes #17 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- dagos becomes a library plus a thin binary; the library holds the
workspace, provider configuration, inspection views, and HTTP server.
- Read-only views assembled from recorded state (core domain types, so
they serialize per the contracts): Overview (project, run defaults,
providers, Jev in use, the durable DAG, every run with its message) and
RunDetail (message node, active context with sources, carried nodes,
Jev request and classification, context added/removed, the exact IR,
streamed prose, raw provider output, validated response, emitted nodes
and edges, failure with the rejecting stage and reason, full events).
- `dagos inspect [overview|dag|run|events|context|ir|response] [RUN]`
prints them as JSON; runs are named by ID or `latest`.
- `dagos serve` exposes GET /api/health, /api/overview, and
/api/runs/{id|latest} on loopback (127.0.0.1:7420 by default), with JSON
errors and 404s for unknown runs. Nothing in it mutates state.
- quickstart.md now walks the real commands for each scenario.
- Tests: overview contents, a completed run's full pipeline detail, a
rejected run's explanation and raw output, the HTTP API over a real
socket, and `dagos inspect` through the binary.
Closes #18
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- New crate dagos-mcp: a minimal stdio MCP client (initialize, notifications/initialized, paginated tools/list) that turns server tools into IR capability descriptions named <server>.<tool>. Each server is time-boxed; missing commands, crashes, silence, and non-JSON-RPC output are reported per server and skipped. Nothing is ever called, and no MCP semantics enter dagos-core or DAG state. - Workspaces may list servers in .dagos/mcp.json. `dagos run` and `dagos serve` discover capabilities and compile them into every run's IR via Runtime::with_tools; unavailable servers become warnings and the run proceeds without their tools. Read-only commands never start servers. The overview reports discovered tools and each server's status. - A tiny stdio MCP server fixture binary backs the tests: pagination, every failure mode, discovery statuses, config validation, and full DAGOS runs both without MCP (no tools in IR, run completes) and with it (IR carries exactly the discovered tools). A CLI test proves a run succeeds with its only MCP server unavailable. - README and research.md document the boundary. Closes #19 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Jev calls now have a deadline (Runtime::with_jev_timeout, default 60s):
a hung classifier fails the run with jev_failed instead of hanging it.
FakeJev gains a hanging mode to prove it.
- tests/hardening.rs adds runtime-level coverage beyond the per-layer
suites:
- context and conversation continue across a process restart (fresh
store handle and runtime over the same file);
- a crash mid-run is recovered as `interrupted` and reported to the next
run through IR recent_events;
- a model superseding its earlier decision makes Jev remove the old node
from the next run's context (context.removed) while it stays durable;
- concurrent runs in four projects sharing one store keep contiguous,
ordered, run-scoped event histories;
- 400 seeded random edge insertions never create a cycle, and every
rejection (self-loop, duplicate, cycle) is justified;
- rows tampered below the repository API (invalid IDs, unknown event
types) fail closed on read while other records stay readable.
- Existing suites already cover provider timeout, malformed JSON, invalid
schema, deterministic IR snapshots, context removal, event ordering, and
provider isolation; everything passes locally and in CI on Ubuntu and
Windows.
Closes #20
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Runtime::start/finish split so the server returns a run ID before inference; recover_runs_except leaves runs this process executes alone. - Server: embedded UI assets, POST /api/runs, PUT /api/config, POST /api/recover, SSE /api/stream of committed events, loopback Host-header guard, CSP on assets. - UI (vanilla ES modules, no build step): run list, numbered pipeline stages, conversation/context/Jev/IR/response/events tabs, durable DAG arc diagram with temporary active-context rings. UI tests and Spec Kit convergence still to come. Refs #21 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- dagos-openai: OpenAiCompatibleJev classifies context through any OpenAI-compatible endpoint (temperature 0, classifier-only prompt plus the kiss.jev-context.v1 schema); output is still validated by the runtime - dagos: select the Jev via DAGOS_JEV_PROVIDER/DAGOS_JEV_MODEL or the providers.json `jev` entry; the offline fake stays the default - tests: model-backed Jev run over the mock endpoint, a planning Jev is rejected with jev_invalid_output, optional live Jev in the ignored test - README: OpenRouter Jev and inference setup Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… Jev - keys: per-user keys file outside every project (APPDATA / XDG config, owner-only, atomic writes, corrupt files never overwritten); keys never reach the project, database, IR, or API responses, only hints (sk-o…7890) - providers: OpenRouter, OpenAI, and Z.ai presets, available with an env or saved key (env wins); custom OpenAI-compatible endpoints editable in-app; connection check validates keys and lists models - runtime: optional Jev fallback; a failed, timed-out, or rejected model Jev is recorded (jev.rejected, jev.fallback) and the offline policy classifies the same request, so runs never depend on the model Jev - workspace: provider configuration reloads without a restart; in-flight runs keep the runtime they started with - server: /api/settings endpoints for keys, endpoints, checks, and the Jev - ui: Providers & keys dialog (s / ⚙): save & test keys, pick models for runs, add endpoints, choose the Jev; inspector shows Jev fallbacks - cli: `dagos keys [list|set|remove]`, set reads the key from stdin - spec: FR-021 (optional model Jev), FR-022 (in-app key management) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Decisions models (`~typesafe/jev-latest`, `typesafe/jev-*`) reject Chat Completions, so a configured decisions model now uses POST /api/alpha/decisions: one calibrated `noul` question per candidate node, answers mapped to active (p >= 0.5) or inactive in a kiss.jev-context.v1 document that the runtime validates as before. Malformed answers fail the Jev and fall back to the offline policy. The settings Jev picker recommends TypeSafe's Jev on OpenRouter. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- store: migration 0003 adds conversations (rename/archive, never delete)
and runs.conversation_id; earlier runs become one conversation per
project titled after its first message
- runtime: Thread::{Latest, Conversation, New}; context carries and the
IR's recent turns stay within a conversation while the project DAG is
shared by all of its conversations
- ir: recent_events is the conversation so far (last 8 turns), each with
the user's request and the reply prose or failure (additive `request`)
- jev: Decisions questions carry recency and the latest user messages, so
"what did we just discuss" keeps the latest turns active
- server: projects (list/create/rename/overview/config), conversations
(view as turns, rename, archive), runs target a conversation, a project,
or a new conversation; run IDs resolve across projects
- cli: `dagos run --new` starts a new conversation
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- chat view: conversations as threads (user bubbles, streamed replies, safe markdown, inline failures, per-turn context/Jev/emission details and an Inspect link); composer with model chip and config popover - sidebar: new chat, chats by recent activity, archived chats; header rename and archive/restore - project switcher: switch, create, and rename projects, each with its own DAG and run configuration - inspect view: the existing run inspector with turn navigation - keyboard: n new chat, v chat/inspect, p projects, j/k chats or turns - tests: node --test UI suite for chat rendering (escaping, markdown, turns, menus), run in CI - docs: README app section, FR-023, data model for conversations Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- core: tools layer (ToolGate, ToolExecutor); runs execute the tool calls their validated responses request, up to a step limit, recording tool.requested / tool.decided / tool.completed events; results reach the model through IR tool_results; fake-tool model for offline tests - mcp: long-lived stdio sessions with tools/call, per-tool policies (off/ask/allow), server cwd/enabled, policy changes without restarts, image data kept out of IR, long text truncated - app: approval gate (policy first, then a person in the app, 10 min timeout; the CLI denies `ask` calls), API to answer calls and manage servers and policies, turn items interleaving prose and tool calls - protocol: models are told how to request tools and read results - constitution: principle 7 amended for bounded, approved tool execution Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- chat: turns interleave replies and tool calls; calls waiting for a person show Allow once / Always allow / Deny; cards show arguments, results, errors, and denial reasons, and stay open across refreshes - settings: Tools section per MCP server with status, per-tool and bulk off/ask/allow policies, a tool filter, launch settings, and one-click import of Claude Desktop's servers (no environment values copied) - serve starts MCP servers in the background; start timeout 30s - api: pending flag on calls actually waiting for a person - tests: UI tests for tool cards and Tools settings; import test - docs: README tools section Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…put fences Tool-calling chat models sometimes write a one-line preamble before the JSON document (repeating presentation.prose) or fence the whole reply. The adapter now removes either when nothing is lost; any other output is returned unchanged and still fails validation. The protocol also tells models to keep tool-step notes inside presentation.prose. Verified live: openai/gpt-6-luna via OpenRouter called ooda.get_system_info through an approval and answered from its result. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ment gpt-6-luna paraphrases its prose before the JSON whenever it calls several tools, so requiring the preamble to repeat the prose was too strict. A preamble is now removed when it holds no JSON, is at most 1000 characters, and everything after it is exactly one JSON object; other output still fails validation. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- markdown.js: block parser with nested and ordered lists, blockquotes, tables, rules, headings, fences (also inside list items), and inline code, bold, italics, strikethrough, links; everything escaped first - tool results render as markdown (OODA's output is markdown), dropping a leading heading that repeats the tool name; structured-only output stays JSON - three or more consecutive tool calls fold into one group with a status summary, open while a call waits for approval - sidebar no longer scrolls sideways; long chat titles truncate - tests: markdown suite, including OODA output Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Full debug info in every test binary grew the build directory to 36 GB and filled the disk. A full clippy + test build now takes 2.3 GB. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
All enabled tools are offered to Jev with short descriptions; Jev labels tools active or inactive like nodes, and only tools it does not hide are compiled into the turn's IR. TypeSafe's Jev answers one noul question per tool (keyed tool:<name>). Unlabeled tools stay exposed, so the offline policy (and any Jev failure) keeps today's behaviour. Tool permission is unchanged: policies and the person still decide whether a call runs. - contracts: optional `tools` in kiss.jev-request.v1 and kiss.jev-context.v1 - validation rejects unknown or duplicate tool labels - ui: turns show "tools N/M"; the Jev tab lists each tool's label - constitution amendment, FR-024, README Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
IR keeps only the conversation's latest turns. When a conversation is longer, runs offer the built-in dagos.recall tool: every earlier turn is one chunk, Jev judges each chunk's relevance to the model's query (one noul question per turn through the Decisions API, batched to judge-sized requests; word overlap without a model Jev or when it fails), and the relevant turns come back verbatim with before/after cursors. Recall reads only the run's own conversation, so it needs no approval. `dagos` is reserved as an MCP server id. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…utomatically Replaces the dagos.recall tool: recall is DAGOS's job, not something the model has to think of. Chats organize work for people; the project remembers. - Every run, Jev judges each earlier turn of every chat in the project that the IR's recent window does not carry (one noul question per turn through the Decisions API); the most relevant reach the IR verbatim as `recalled`. - Before each step of a run, Jev judges every large tool result from earlier rounds against the request and the model's latest step. Results it judges not needed are replaced by a note in the IR and come back when relevant again; the latest round is always kept and full outputs stay in the event history. - Without a model Jev that judges relevance, or when it fails, nothing is recalled and nothing is left out. - The chat shows "recalled N" and "N results left out" under a reply. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Syntax linters cannot check meaning; this one does. Rules are plain-English sentences in .dagos/lint.json (14 defaults: "Swallows errors.", "Has a hidden side effect.", ...), and Jev answers one calibrated yes/no question per rule for each function, all functions in parallel, cached by content. The review loop: a model reaches files only through tool calls, so before each permitted call the runtime shows it to the reviewer, which remembers the project files it names (paths, and file names inside shell commands). Each time the model replies without tool calls, the functions it changed are judged; findings at or above the threshold go back to the model as `review` in the IR and the run continues, until a review is clean, the code stops changing, or max_rounds (3) reviews have run. Reviews are recorded as review.completed and shown in the chat; they never fail a run. - dagos-core: Reviewer interface, bounded review loop, IR `review`, the general JevClassifier::decide (yes/no questions), review events. - dagos-openai: decide through the Decisions API. - dagos-lint (new): function extraction for Rust, JS/TS, and Python, rules, judging with a cache, LintReviewer. - dagos: the reviewer wired from lint.json, `dagos lint [files]` for CI (exit 1 on findings, --json for every judgment), review cards in the chat. - Constitution amended for the bounded loop; FR-026. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… `{`
A preamble that mentioned braces in prose ("returns `{}` on errors") made the
whole reply fail: the document was searched from the first `{` anywhere. It now
starts at the first line beginning with `{` that parses as a whole object; a
preamble may not itself begin with `{`, so only presentation text is dropped.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…olicies Tool checks only ever make a call stricter than its policy: - Meta-tools (tools that run other tools, e.g. OODA's batch_tools) get the strictest policy among themselves and every tool of the same server their arguments name: naming an off tool refuses the call, naming an ask tool asks. - The guard: with a Jev that answers yes/no questions, each call about to run is judged against eight risks in the light of the user's request (destroys data, changes the system, uses the network, touches secrets, changes files outside the project, stops other processes, controls the computer, delegates to tools chosen at run time). A risk at 0.5 or more, or a check that cannot run, turns allow into a question; the approval card says why, and the reason is recorded in tool.decided with the person's decision. Without such a Jev, policies decide alone. ToolDecision::Allow now carries an optional note. FR-027; constitution note. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Risks now say whether the user's request can excuse them. Changing requested files, installing requested software, and the like pass when the request plainly asks for them (the lint loop rewrites files constantly); deleting data (rm, del, Remove-Item, DROP, git clean, ...), touching secrets, and delegating to tools chosen at run time always go to the person. Nine risks in all. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rson Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- spec: user stories and acceptance scenarios for chats, tools, recall, and the review loop; scope now names the two bounded, amended loops instead of ruling out all loops. - plan: the real architecture, crates, and converged phase; the constitution gate states the bounded loops. - research: MCP tools now run under policies; decisions since the core (Jev as yes/no judgments, recall over summaries, the tool-call hook, checks only tighten, the small extractor, tolerated model quirks); unresolved tradeoffs. - data-model: event types, what the IR carries and from where, workspace files. - quickstart: the app, a real model and Jev, tools, lint. - tasks: T033/T034 done; T036-T045 for the converged scope; T035 (tag) open. - checklists: requirements checked; new traceability of FR-001..027 to code and tests, with the boundary checks re-verified. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ll cost From a high-effort review of PR #22; each finding was checked against the code. Security - Meta-tools could run `off` tools: the gate only knew offered tools. It now learns every tool the servers describe, `off` ones included. - `serve` refuses non-loopback addresses: the app runs tools and has no sign-in, and a LAN bind let anyone add a server that runs any command. - The guard judged only the first 6000 characters of arguments. Long arguments are now judged in overlapping windows (a risk counts in any window), and arguments too long to window fail closed and ask. - A preamble holding a JSON object is no longer dropped as "presentation text". Correctness - The streaming prose extractor starts at a `{` that begins a line, like unwrap_document, so a preamble mentioning braces no longer blanks live prose. - Starting a run and recovering stale runs are serialized, so recovery cannot fail a run between its creation and being marked executing. - "Always allow" also allows the tools a meta-tool named that made it ask; a guard-flagged call cannot be remembered (it asks every time), and the button is hidden for it. - Edits of mcp.json hold a lock from load to save. Performance - The MCP pool lock is no longer held while a session is busy or starting, so a slow call on one server does not stall every other server. - Recall and compaction read only the event types they use, never the many inference.delta events (new Tx::events_of_types). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A run races its pipeline against a stop signal: stopping drops whatever it is
waiting for (the provider, a tool, a person's approval, Jev) and records
run.failed with the new error code `cancelled`; everything committed before
stays. POST /api/runs/{run}/stop reaches the runtime executing the run even
after a settings reload. Approvals forget a wait that is dropped, and calls left
unfinished by an ended run show as stopped. In the app, Stop replaces Send while
the chat's reply is running (Esc also stops it).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Settings → Safety and quality has two new panes. Review loop: on/off, the finding threshold, reviews per run, and the rules themselves (text plus optional "applies when" / "not a problem when" hints; add, remove, restore the defaults), saved to lint.json and used by the next runs. Tool guard: on/off and its threshold, saved to mcp.json (`guard`), with the nine risks listed and the ones that always ask marked. New endpoints: GET/PUT /api/lint and PUT /api/tools/guard; both validate before saving. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every chat message is a `conversation` node, and they crowded out what runs
recorded. The panel now hides them by default; a header toggle ("Show 12
messages") brings them back and is remembered in this browser, a selected
message stays visible, and the counts still cover the whole DAG.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A third view next to Chat and Inspect (`l`). It judges the files changed since the last commit, or the files named, and shows every judgment as a heatmap of functions × rules, findings outlined. Selecting a function shows its source and each rule's probability, most likely first. A finding can be marked "not a problem here": dismissals live in lint.json and the review loop and `dagos lint` skip them; Restore brings one back. New endpoints: POST /api/lint/run, PUT /api/lint/dismissals. Changed-file detection moved to the app library, shared by the CLI and the view. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When the review loop ends with findings still open (the model disputed them, the code stopped changing, or the run ran out of reviews), each is recorded as an observation node (`kind: "lint_finding"`, with rule, file, function, line, and probability) and announced with dag.node_created. A finding the project already has a node for is not recorded again. Later runs in any chat draw on them through Jev's classification. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The OpenAI-compatible adapter now offers the IR's tools through the endpoint's own `tools` field (names like `ooda.exec_cli` mapped to allowed function names and back) instead of in the IR text, assembles the model's streamed native tool calls, and returns them as the response document's `tool_calls`, with any note the model wrote as prose (streamed live). Runtime, gate, and guard see the same validated document as before. Final replies still use the JSON document. A model or endpoint that refuses tools (a 400/404/422 mentioning tools or functions) is retried with DAGOS's JSON protocol and remembered, so the next steps skip the attempt. Endpoints can opt out with `"native_tools": false`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ard paths - The review loop also judges code files a run's calls changed without naming them (e.g. a generator a shell command runs), found by modification time since the run's first call (build and tool folders skipped), judged whole. - Judged answers are saved in .dagos/lint-cache.json by the review loop, the Lint view, and `dagos lint`, so unchanged functions are not judged again after a restart. - A run started while MCP servers are still starting waits for them (up to 45 s) and runs with the tools; Stop works during the wait (the run ends as cancelled without calling the model). - The guard sees the project folder in both path styles and without Windows' verbatim prefix, which caused an `outside-project` false positive for paths inside the project. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Context is managed by relevance, not rationed. Recall no longer keeps only the 5 most relevant turns, no longer searches only the latest 300, and no longer cuts a recalled turn to 4000 characters or its tool results to excerpts: every earlier turn Jev judges relevant reaches the IR whole. Jev still judges a trimmed copy of each turn (its own input limit), which never limits what the model receives. Long histories are judged in parallel Decisions batches, so they take no longer than short ones. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ests The model's context window is the one real limit, so it is managed instead of failing: providers may report a model's window (the OpenAI-compatible adapter reads OpenRouter's model list once), and when everything relevant does not fit, the runtime fits each step's IR to it, leaving out the least relevant recalled turns first, then earlier rounds' tool outputs (oldest first), then the oldest chat turns. Nothing is left out while everything fits, and without a known window nothing changes. What still cannot fit is sent trimmed as far as possible. Tests: the inspector and tools API tests no longer read the real per-user key file (they were not hermetic), the inspector test gets a generous deadline for busy CI runners, and a run that fails there now says why. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Jev now also answers one yes/no question about the person's own message: does it ask for all available context rather than about something specific? On a yes, the run loads every node of the project into its context and recalls every earlier turn of every chat (newest ranked highest), fitted to the model's window like any other step, and records context.expanded; the reply shows "full context". Only the person's message is judged, never tool output, and without a Jev that answers yes/no questions nothing changes. Constitution note. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: e0eb553463
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| let stopped = | ||
| task_state.stop_requests.lock().unwrap_or_else(PoisonError::into_inner).remove(&id); |
There was a problem hiding this comment.
Keep stop requests visible until Runtime::finish registers
When POST /api/runs/{run}/stop arrives after this removal but before runtime.finish(started) inserts the run's Notify, runtime.stop(&run) returns false and the newly inserted stop_requests entry is never checked again. The worker then runs normally and discards that request at cleanup, so an API call reported as {"stopping": ...} can still proceed to execute allowed tool calls.
Useful? React with 👍 / 👎.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
DAGOS 0→1: the core runtime from the Spec Kit backlog (#2–#20), converged with the implementation (#21), plus the scope the project owner added along the way. Every expansion beyond the original brief is recorded as a constitution amendment in
.specify/memory/constitution.md.What is in it
dagos-core): durable DAG in SQLite → Jev classifies active context → versioned IR → provider → fail-closed validation → DAG emissions, with every transition an ordered event. Layer boundaries are enforced bytests/boundaries.rs.dagos-openai): OpenAI-compatible endpoints (OpenRouter, OpenAI, Ollama, …), native tool calling with a remembered fallback to the JSON protocol, TypeSafe's Jev through the Decisions API, and an offline Jev fallback.dagos-mcp):dagos-lint):dagos lintfor CI, and open findings remembered as nodes.dagos serve, loopback only): chat, inspector, DAG panel, Lint view, and settings for providers, keys, Jev, tools, the review loop and the guard.How to review
specs/001-core-runtime/checklists/traceability.mdmaps every requirement (FR-001…FR-027) to its implementation and tests.research.mdrecords the design decisions and the unresolved tradeoffs.655c1d0.Verification
cargo test --workspace(258 tests) and the UI tests (29) pass locally. CI passes on Linux and Windows.Closes #21
Closes #1
🤖 Generated with Claude Code