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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 86 additions & 0 deletions docs/plan/M2.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# Milestone M2: domain and loop

- Status: In progress — `chat` mode, the loop-to-`RunBloc` application service, and the section 2.8 metrics remain.
- Specification: `docs/spec/domain.md` (sections 8–9), `docs/spec/storage.md` (section 15), `docs/spec/roadmap.md` (M2), `docs/spec/dart-language.md` (section 7)
- ADR: — (no decision changed; the discrepancies found during implementation are recorded as Q7–Q14 in `docs/spec/open-questions.md`)

## Acceptance criteria

- [x] `Run.addTaint` adds a label idempotently; the `taints` getter is a read-only view, the constructor copies its input, and the run never removes a label (`runtime/core/test/domain/run_test.dart`).
- [x] `HiveJournal` implements `JournalPort`: the `seq` grows across calls and reopen, and `forRun` filters by run (`runtime/core/test/infrastructure/hive_journal_test.dart`).
- [x] `runAgent` follows the fixed section 9 order: malformed and unavailable checks, policy, approval, journal `started` before the tool call, timeout with a per-invocation child token, taint on untrusted output, non-throwing truncation, fresh tool snapshot per step, cancellation; 24 tests (`runtime/core/test/application/agent_loop_test.dart`).
- [x] `truncateToolOutput` measures UTF-8 bytes, cuts on a rune boundary, and JSON-encodes non-strings (`runtime/core/test/application/tool_output_test.dart`); `ToolSet` matches on the LLM name (`runtime/core/test/application/tool_set_test.dart`).
- [x] Fail-closed agent defaults are registered in `CoreModule`; `registerJournal({required String directory})` registers the journal after `configureCore` and rejects a second registration; `AgentDeps` resolves from the registered collaborators (`runtime/core/test/di/injection_test.dart`).
- [x] `dart run melos run ci` is green (format, analyze, arch check, tests) and `dart run tools/docs_check/bin/docs_check.dart` exits 0.
- [ ] `chat` mode runs end-to-end against a fake `LlmPort` (roadmap M2).
- [ ] The application service binds the loop to `RunBloc`, projects journal state, and publishes `AgentEvent`s through `StatePublisher` (sections 8.10 and 8.11).
- [ ] The section 2.8 target metrics are measured (not measurable before the daemon exists; postponed to M4).

## Steps

- [x] Step 1: `Run` aggregate — `RunMode`, `Run`, a read-only `taints` view over copied labels, `Run.addTaint` in `runtime/core/lib/src/domain/run.dart`, exported from `package:alteri_one_core`; tests in `runtime/core/test/domain/run_test.dart`.
- [x] Step 2: Hive-backed journal — `runtime/core/lib/src/infrastructure/journal/hive_journal.dart`, implementing `started`, `denied`, `finished`, and `forRun` with a shared sequence counter; tests in `runtime/core/test/infrastructure/hive_journal_test.dart`. The constructor uses `new _` (fix for `unnecessary_type_name_in_constructor`).
- [x] Step 3 (added): application/agent seam types in `runtime/core/lib/src/application/agent/` — `RunResult` (`RunCompleted`, `RunBudgetExceeded`), `ToolSet` and `ToolRegistry`, `AgentConfig`, `ApprovalService`, `ContextBuilder`, `AgentDeps`, and internal `truncateToolOutput`; tests in `runtime/core/test/application/tool_set_test.dart` and `runtime/core/test/application/tool_output_test.dart`.
- [x] Step 4 (added): section 9 loop — `runAgent` in `runtime/core/lib/src/application/agent/agent_loop.dart`; 24 tests in `runtime/core/test/application/agent_loop_test.dart` with the fakes in `runtime/core/test/application/agent_fakes.dart`.
- [x] Step 5 (added): fail-closed defaults in `runtime/core/lib/src/application/agent/agent_defaults.dart` (`DefaultAgentConfig`, `DenyAllApprovals`, `PromptContextBuilder`, `EmptyToolRegistry`), `AgentDeps` binding in `runtime/core/lib/src/di/core_module.dart`, and `registerJournal({required String directory})` in `runtime/core/lib/src/di/injection.dart`; tests in `runtime/core/test/di/injection_test.dart`.
- [x] Step 6 (added): rewrite this plan in the `_template.md` form and record the M2 open questions (Q7–Q14) in `docs/spec/open-questions.md`.
- [ ] Step 7 (added): `chat` mode end-to-end with a fake `LlmPort`; a test drives a conversation through the loop and checks the final reply and the journal. Gated by Q13: the model-call journaling decision must land with this step, not be postponed again.
- [ ] Step 8 (added): application service that binds `runAgent` to `RunBloc`; it projects run state from the journal and publishes `AgentEvent`s through `StatePublisher` (sections 8.10, 8.11). Gated by Q13: the service journals model calls and transcript persistence.
- [ ] Step 9 (added): section 14 profile and risk configuration plus an interactive approval channel; replaces `DefaultAgentConfig` and `DenyAllApprovals`. Profile rules must never lower a declared risk.
- [ ] Step 10 (added): section 13 untrusted wrapping — 128-bit random boundary per run, `untrusted_` variables, the injection test with a `write` tool, and the tool-output boundary wrapping deferred from the loop (Q8). Until then `PromptContextBuilder` passes the prompt through unwrapped and tool output reaches the model as the truncated plain value.
- [ ] Step 11 (added): section 2.8 target metrics (cold start, idle memory, event latency p99, tool-call overhead). This needs the daemon and is therefore done in M4.

## Open checks [VERIFY]

- [ ] T2: schemantic annotations on SDK 3.13 — partially resolved; `ToolSpec` and the other DTOs stay hand-written, the spike remains for edge cases.
- [ ] T3: `returnToolRequests` with `genkit_openai` and `llama-server` — the loop relies on it; the adapter lands in M3.
- [ ] T4: `genkit_openai` behavior with invalid tool arguments — the loop marks such calls `malformed`; the adapter lands in M3.
- [ ] T5: `response_format` with JSON Schema on the `llama-server` build — environment gap, no server installed.
- [ ] T6: the complete `llama-server` tool and web-UI flag list — environment gap, no server installed.
- [ ] T8: handlebars_dart strict mode through dotprompt — affects the section 13 wrapping (Step 10).
- [ ] T10: the `SKILL.md` format in `genkit_middleware` (M5).
- [ ] T11: `dart build cli` on Dart 3.13 for each OS (M4).
- [ ] T13: the `shelf_io.serve` signature with `securityContext` (M4b).
- [ ] T15: `StatePublisher` queue limits and overflow behavior (M5) — relevant to Step 8.
- [ ] T16: `mcp_dart` client API and protocol version (M8).
- [ ] T17: `dart language-server` pull diagnostics and position encodings (M7).
- [ ] T18: context compaction keeps goals and open items (M6).
- [ ] T19: system key-storage libraries and Linux behavior without Secret Service.
- [ ] T20: Windows logon-task shutdown and `%APPDATA%` mapping (M4).
- [ ] Q7: mutable `Run` versus copy-with for taint labels — see `../spec/open-questions.md`.
- [ ] Q8: the exact `_truncateUntrusted` contract — see `../spec/open-questions.md`.
- [ ] Q9: types of `d.config`, `d.registry`, `d.context`, `d.approvals` — see `../spec/open-questions.md`.
- [ ] Q10: journal directory resolution versus a synchronous `configureCore` — see `../spec/open-questions.md`.
- [ ] Q11: enforcement of the budget beyond `maxSteps` — see `../spec/open-questions.md`.
- [ ] Q12: journaling an approval refusal — see `../spec/open-questions.md`.
- [ ] Q13 (gates Steps 7–8): model-call journaling and transcript persistence (`JournalPort` or application-service decorator) — see `../spec/open-questions.md`.
- [ ] Q14: `unknownOutcome` versus `failed` for a timed-out, uncancellable tool — see `../spec/open-questions.md`.

## Outcome

The domain and loop groundwork landed on `feat/m2-run-aggregate` (draft PR #41): the `Run` aggregate with a read-only `taints` view and `addTaint`, the Hive journal and its registration hook, the `application/agent/` seam types, the section 9 `runAgent` loop with per-invocation child tokens and non-throwing truncation, fail-closed defaults and their DI bindings.

Postponed within M2: `chat` mode end-to-end, the application service that binds the loop to `RunBloc` and `StatePublisher`, section 14 configuration and the interactive approval channel, and section 13 untrusted prompt and tool-output wrapping. The section 2.8 metrics are postponed to M4 because there is no daemon to measure yet.

Decisions the specification does not settle are recorded as Q7–Q14 in [open-questions.md](../spec/open-questions.md); `docs/spec/domain.md` gained only a pointer to Q7, Q8, and Q14, and no decision was changed. Q12 records the observed gap that an approval refusal is not journalled; Q13 records that the loop does not journal model calls, which section 8.10 rule 2 demands (gates plan Steps 7–8); Q14 records that a timed-out call is kept as `failed`.

### Milestone history (M0, M1, M2 step 1)

Recorded before the step 6 rewrite; kept as written.

#### M0 — Done

- Dev tools implemented: `tools/arch_check`, `tools/codegen_check`, `tools/docs_check`, `tools/signing`; CI green; tests pass; `melos run ci` green.
- PR #37 merged (Melos 8 upgrade, lock file removal, CI rewrite), and PR #39 merged (dev tools and M0 spikes). Open M0 checks: T5, T6, T16, T17, T19.

#### M1 — Done

- Core skeleton implemented: contracts, pure domain, domain ports, state components, `RunBloc` and friends, injectable composition root with fail-closed `PolicyGate`.
- `provider_api`, `sdk`, `client` remain scaffolds, to be filled in later milestones.
- PR #40 merged to mark M1 done.

#### M2 step 1 — Done

- Run aggregate added: `enum RunMode { chat, task, resident }` and `final class Run` in `runtime/core/lib/src/domain/run.dart`, exported from `package:alteri_one_core/alteri_one_core.dart`.
- Hive-backed `JournalPort` implementation added at `runtime/core/lib/src/infrastructure/journal/hive_journal.dart`.
- Unit tests added: `runtime/core/test/domain/run_test.dart` and `runtime/core/test/infrastructure/hive_journal_test.dart`.
2 changes: 2 additions & 0 deletions docs/spec/domain.md
Original file line number Diff line number Diff line change
Expand Up @@ -326,4 +326,6 @@ Future<ToolResultBlock> _invoke(Run run, ToolSet tools, ToolCall call, AgentDeps

Tool outputs are always untrusted and are truncated to `maxOutputBytes`. The full error text is not passed to the model context, so internal details do not leak.

Implementation note: the following details of the pseudo-code above differ deliberately. `Run.addTaint` stands in for `run.taints.add` (Q7). Cancellation is re-checked after the model call and again after the tool batch, before a result is returned. Both versions truncate after the journal write; the difference is that taint application and truncation run outside the `try`, so neither can rewrite an already-written `succeeded` record (Q8). A timed-out call stays `failed` (Q14). See `open-questions.md`.

Generation through genkit: a `generate` call with `returnToolRequests: true` returns tool requests without executing them. Parameter names are checked against genkit 1.x during implementation [VERIFY]. The adapter in `alteri_one_provider_llama_cpp` converts the response into a `Turn`. A `ToolCall` with invalid arguments is marked `malformed` without failing the run.
10 changes: 9 additions & 1 deletion docs/spec/open-questions.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

## 25.1 Questions for the owner

Added during the M0 scaffold (2026-10-17). Until answered, the scaffold takes the conservative choice listed under each question.
Added during the M0 scaffold (2026-10-17); Q7–Q14 added during M2 (2026-11-08). Until answered, the scaffold takes the conservative choice listed under each question.

| ID | Question | Conservative choice taken |
|---|---|---|
Expand All @@ -14,6 +14,14 @@ Added during the M0 scaffold (2026-10-17). Until answered, the scaffold takes th
| Q4 | ~~Repository URL: the spec gives the repository name `alteri-one` but not the GitHub owner.~~ Resolved 2026-10-17: the owner is `dip-develop`; all pubspecs and docs use `https://github.com/dip-develop/alteri-one`. | resolved |
| Q5 | `public_member_api_docs` cannot coexist with the primary-constructor style mandated by section 7.2: the Dart 3.13.5 analyzer does not attach `///` comments to primary-constructor parameters (the style used by every example in `domain.md` section 8). Re-enable the lint when the analyzer supports parameter docs. Until then, parameter dartdoc stays mandatory by convention; `packages/AGENTS.md` ("public classes and methods have English dartdoc") is enforced in review, not by the analyzer. | lint omitted from the shared set; docs kept by convention |
| Q6 | Section 7.3 shows named `new` constructors with a dot (`new origin()`); the Dart 3.13.5 compiler rejects the dot and requires a space (`new origin() : x = 0, y = 0;`), named factories are written without the class name (`factory standard() { ... }`), and named `new` factories are illegal. Update section 7.3 with the compiler-verified forms. | code follows the compiler forms probed on 3.13.5 |
| Q7 | Section 9 shows `run.taints.add(...)` (section 8.2 does not), but `Run` defaults to `const {}` and the spec does not say whether `Run` is a mutable aggregate or copy-with. Confirm the mutable form and that the run itself never clears a label. | read-only `taints` view plus `Run.addTaint`; only the owner clears a label |
| Q8 | `_truncateUntrusted` is unspecified: UTF-8 bytes or characters, JSON encoding for non-strings, a cut marker, a fallback for values `jsonEncode` rejects, and whether the section 13 boundary wrapper belongs in it. | UTF-8 bytes, rune-safe cut, `jsonEncode` for non-strings, a bounded `[unserializable output]` fallback, no marker or wrapper in M2 |
| Q9 | Section 9 uses `d.config`, `d.registry`, `d.context`, and `d.approvals` without giving their types. | minimal application-layer interfaces in `application/agent/`; no section 14 loading in M2 |
| Q10 | `configureCore` is synchronous and the journal needs a directory; section 18.3 path resolution is M4. Confirm who resolves the path. | `registerJournal(directory:)` after `configureCore`; the caller owns path and lifetime, no home-directory default |
| Q11 | Section 9 enforces only `maxSteps`, while `Budget` also has `maxTokens`, `maxDuration`, and `maxOutputTokens`; `Turn` carries no usage yet, so the loop cannot enforce them. | enforce `maxSteps` only; revisit when the provider reports usage (M3) |
| Q12 | An approval refusal is not journalled: section 9 writes `denied` only for policy denials, so `not_approved` leaves no audit record although it stops a side effect. Should a refusal be recorded? | keep the spec order, no extra record in M2; add an approval outcome record when the interactive channel lands (M4/M9) |
| Q13 | Section 8.10 rule 2 lists "model call, tool call, channel send" as effects that must be journalled before they run, but the section 9 loop calls `d.llm.complete(...)` without a journal record and the section 9 snippet omits one. Is model-call journaling and transcript persistence the `JournalPort`'s job, or a decorator in the application service? | out of the loop for M2; decided together with transcript persistence in plan Step 8 |
| Q14 | A timed-out tool cannot be cancelled by `Future.timeout`. Must a call that may still complete be recorded as `unknownOutcome` instead of `failed`? Section 9 writes `failed`. | keep `failed` (section 9); cancel a per-invocation child token and leave the run token alone |

Previously none were open. Decisions are recorded in ADR-0005, ADR-0006, and ADR-0009.

Expand Down
10 changes: 10 additions & 0 deletions runtime/core/lib/alteri_one_core.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,14 @@ library;

// Application state (docs/spec/domain.md, section 8.10) and
// composition; then the pure domain (sections 8.1–8.9).
export 'src/application/agent/agent_config.dart';
export 'src/application/agent/agent_defaults.dart';
export 'src/application/agent/agent_deps.dart';
export 'src/application/agent/agent_loop.dart';
export 'src/application/agent/approval_service.dart';
export 'src/application/agent/context_builder.dart';
export 'src/application/agent/run_result.dart';
export 'src/application/agent/tool_set.dart';
export 'src/application/approvals_bloc.dart';
export 'src/application/bloc_defaults.dart';
export 'src/application/channels_cubit.dart';
Expand All @@ -32,5 +40,7 @@ export 'src/domain/ports/journal_port.dart';
export 'src/domain/ports/llm_port.dart';
export 'src/domain/ports/memory_port.dart';
export 'src/domain/ports/tool_port.dart';
export 'src/domain/run.dart';
export 'src/domain/run_state.dart';
export 'src/domain/trigger.dart';
export 'src/infrastructure/journal/hive_journal.dart';
16 changes: 16 additions & 0 deletions runtime/core/lib/src/application/agent/agent_config.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// Copyright 2026 The AlteriOne Authors.
// SPDX-License-Identifier: Apache-2.0

import 'package:alteri_one_contracts/alteri_one_contracts.dart';

/// Per-profile tool configuration seen by the agent loop
/// (docs/spec/domain.md, section 9).
///
/// The loop pairs the result with the declared risk through
/// [effectiveRisk]: configuration may raise a tool's risk, never lower it.
abstract interface class AgentConfig {
/// The configured risk of the tool with the canonical [toolName]
/// ([ToolSpec.name], not [ToolSpec.llmName]), or `null` when the profile
/// does not override the declared risk.
Risk? riskFor(String toolName);
}
67 changes: 67 additions & 0 deletions runtime/core/lib/src/application/agent/agent_defaults.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
// Copyright 2026 The AlteriOne Authors.
// SPDX-License-Identifier: Apache-2.0

import 'package:alteri_one_contracts/alteri_one_contracts.dart';

import '../../domain/messages.dart';
import '../../domain/run.dart';
import 'agent_config.dart';
import 'approval_service.dart';
import 'context_builder.dart';
import 'tool_set.dart';

/// Fail-closed placeholders for the agent-loop collaborators
/// (docs/spec/domain.md, section 9).
///
/// They keep the composition root resolvable before the matching features
/// land: profile configuration (section 14), the interactive approval channel
/// (M4/M9), prompt rendering (section 13), and extensions (M5). Each one
/// refuses or narrows rather than guessing, in line with the fail-closed
/// defaults of the rest of the core.

/// An [AgentConfig] without overrides: every tool keeps its declared risk.
final class DefaultAgentConfig implements AgentConfig {
/// Creates the defaults.
const new();

@override
Risk? riskFor(String toolName) => null;
}

/// An [ApprovalService] that refuses every request.
///
/// There is no interactive channel until M4/M9, so an `ask` decision always
/// ends as `not_approved` (docs/spec/domain.md, section 8.8).
final class DenyAllApprovals implements ApprovalService {
/// Creates the defaults.
const new();

@override
Future<bool> ask(Run run, ToolSpec tool, Map<String, Object?> input) =>
Future.value(false);
}

/// A [ContextBuilder] that starts the run from its prompt alone.
///
/// System instructions and context fragments are rendered in section 13; until
/// then the owner prompt is the whole conversation.
final class PromptContextBuilder implements ContextBuilder {
/// Creates the defaults.
const new();

@override
Future<List<Message>> build(Run run) =>
Future.value([Message.user(run.prompt)]);
}

/// A [ToolRegistry] without tools.
///
/// Extensions land in M5; an empty snapshot makes every tool call fail as
/// `tool_unavailable` instead of running something unconfigured.
final class EmptyToolRegistry implements ToolRegistry {
/// Creates the defaults.
const new();

@override
ToolSet snapshot(String profile) => ToolSet(const []);
}
37 changes: 37 additions & 0 deletions runtime/core/lib/src/application/agent/agent_deps.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// Copyright 2026 The AlteriOne Authors.
// SPDX-License-Identifier: Apache-2.0

import '../../domain/policy.dart';
import '../../domain/ports/journal_port.dart';
import '../../domain/ports/llm_port.dart';
import 'agent_config.dart';
import 'approval_service.dart';
import 'context_builder.dart';
import 'tool_set.dart';

/// The collaborators of the agent loop (docs/spec/domain.md, section 9).
///
/// Pure carrier: the loop holds no state of its own, so every dependency is
/// passed in here.
final class AgentDeps(
/// Model provider.
final LlmPort llm,

/// Source of the tool snapshot for each step.
final ToolRegistry registry,

/// Journal written before every call.
final JournalPort journal,

/// The only component that decides whether a tool runs.
final PolicyGate policy,

/// Owner decisions for [Decision.ask].
final ApprovalService approvals,

/// Per-profile risk overrides.
final AgentConfig config,

/// Starting conversation.
final ContextBuilder context,
);
Loading
Loading