Skip to content
This repository was archived by the owner on Sep 3, 2026. It is now read-only.

Commit 8e7b5b1

Browse files
author
outlndrr
committed
refactor(runtime): flatten config and session flow
- split app config into nested sub-config records - route session mutations through transaction-style commit/apply helpers - keep session snapshots chronological and log memory writeback failures - add CI guard and end-to-end replay coverage
1 parent 464b1e3 commit 8e7b5b1

38 files changed

Lines changed: 1813 additions & 1373 deletions

.github/workflows/ci.yml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,19 @@ jobs:
2222
- name: Format check
2323
run: gleam format --check src test
2424

25+
- name: Guard deprecated patterns
26+
shell: bash
27+
run: |
28+
set -euo pipefail
29+
if rg -n 'should\.equal|io\.debug' src test; then
30+
echo 'Deprecated Gleam test/debug pattern found'
31+
exit 1
32+
fi
33+
if rg -n '\btodo\b' src; then
34+
echo 'Raw todo found in src/'
35+
exit 1
36+
fi
37+
2538
- name: Type check
2639
run: gleam check
2740

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,15 @@
22

33
## Unreleased
44

5+
### Changed
6+
- refactored app config into nested server/storage/provider/agent/tools sub-configs
7+
- flattened config validation helpers and added direct tests for validator rules
8+
- session actor snapshot/state handling now stays chronological internally
9+
- session actor mutations now flow through transaction-style commit/apply helpers
10+
- memory writeback failures are now persisted as `memory.writeback_failed` session events instead of being silently discarded
11+
- centralized session actor/session manager call timeout constants and longer model-adjacent timeouts
12+
- CI now guards against `should.equal`, `io.debug`, and raw `todo` in `src/`
13+
514
### Added
615
- Gleam project bootstrap for `lummy_agent`
716
- root supervisor and supervised Mist HTTP server

CURRENT_IMPROVEMENTS_TODO.md

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
# Current Improvements Todo
2+
3+
Execution checklist for `docs/refactors/2026-04-current-improvements.md`.
4+
5+
## Phase 0 — Baseline & safety net
6+
- [x] Run `gleam format`; keep formatter churn isolated.
7+
- [x] Run `gleam test`; record baseline pass count and timing.
8+
- [x] Add CI guard for `should.equal`, `io.debug`, raw `todo` in `src/`.
9+
- [x] Add end-to-end integration test covering `POST /agent-runs` -> SSE replay/read-back -> event-log read-back.
10+
11+
## Phase 1 — Low-risk polish
12+
- [x] 1.1 Replace hand-rolled list reversal in `src/lummy_agent/app/config.gleam` with `list.reverse`.
13+
- [x] 1.2 Qualify dict imports in `src/lummy_agent/session/session_manager.gleam` and `src/lummy_agent/runtime/event_bus.gleam`.
14+
- [x] 1.3 Replace `panic as` expectation branches in key Result-based tests with `let assert Error(_)` patterns.
15+
- [x] 1.4 Make memory writeback failures observable via session events.
16+
- [x] 1.5 Centralize actor call timeouts in `session_actor.gleam` and `session_manager.gleam`; add longer model-adjacent timeouts.
17+
- [x] 1.6 Document `CollectorMessage` wrapper reason in `session_actor.gleam`.
18+
19+
## Phase 2 — Flatten config validator
20+
- [x] Extract `require_non_blank`, `require_summary_mode`, `require_tool_risk`, `require_secret_key_length` helpers.
21+
- [x] Rewrite `validate/1` with flat `result.try` flow.
22+
- [x] Add direct tests for validator helpers.
23+
24+
## Phase 3 — Split config into sub-configs
25+
- [x] Define `ServerConfig`, `StorageConfig`, `ProviderConfig`, `AgentConfig`, `ToolsConfig`.
26+
- [x] Update `defaults/0` and `from_env/1` to populate sub-configs.
27+
- [x] Migrate config consumers to nested fields.
28+
- [x] Narrow function signatures to smaller sub-configs where practical (`prompt_builder`, `planner`, `memory_*`, provider registry/adapters, tool runtime helpers).
29+
- [x] Remove flat config fields.
30+
31+
## Phase 4 — Fix snapshot list direction
32+
- [x] Make `Snapshot.messages`, `Snapshot.runs`, `Snapshot.events` internally chronological.
33+
- [x] Remove `public_snapshot/1` and `chronological_messages/1`.
34+
- [x] Update call sites to append chronologically.
35+
- [x] Verify event-log ordering and replay ordering stay stable.
36+
37+
## Phase 5 — Extract transaction pipeline in `session_actor`
38+
- [x] Define `SessionTransaction` and `RunActorSync`.
39+
- [x] Add transaction builders, `commit_and_apply`, `fold_into_snapshot`, `apply_run_actor_syncs`.
40+
- [x] Migrate `append_message`.
41+
- [x] Migrate `start_run_impl`.
42+
- [x] Migrate `start_tool_call_impl`.
43+
- [x] Migrate `succeed_tool_call_impl`.
44+
- [x] Migrate `fail_tool_call_impl`.
45+
- [x] Migrate `apply_tool_result_impl`.
46+
- [x] Migrate `execute_model_turn_impl` adjunct flow (`maybe_update_run_plan`, chunk event handling where applicable).
47+
- [x] Migrate `transition_run_impl`.
48+
- [x] Migrate `complete_run_impl`.
49+
- [x] Migrate `complete_run_with_assistant_message_impl`.
50+
- [x] Migrate `fail_run_impl`.
51+
- [x] Migrate `cancel_run_impl`.
52+
- [x] Delete superseded pre-transaction helper paths.
53+
54+
## Phase 6 — Record spread consistency
55+
- [x] Replace remaining manual `State(...)` rebuilds with record spread where clearer.
56+
57+
## Phase 7 — Repository pattern decision
58+
- [x] Document repository pattern decision; keep record-of-functions for now (`docs/adr/0001-repository-pattern.md`).
59+
60+
## Phase 8 — Cleanup & validation
61+
- [x] Re-run `gleam format`.
62+
- [x] Re-run full test suite.
63+
- [x] Audit line counts.
64+
- target: `session_actor < 1200`; actual: `1776`
65+
- target: `config < 400`; actual: `564`
66+
- [x] Update `docs/session-runtime.md` for transaction pipeline.
67+
- [x] Update `CHANGELOG.md` with refactor + writeback observability note.
68+
- [x] Move `CURRENT_IMPROVEMENTS_PLAN.md` to `docs/refactors/2026-04-current-improvements.md`.
69+
70+
## Baseline notes
71+
- [x] Baseline recorded.
72+
- Pass count: `98`
73+
- Test timing: `15.213s`
74+
75+
## Final validation
76+
- [x] Final `gleam check`
77+
- [x] Final `gleam test`
78+
- Pass count: `101`
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# ADR 0001: Keep repository record-of-functions for now
2+
3+
## Status
4+
Accepted
5+
6+
## Date
7+
2026-04-18
8+
9+
## Context
10+
Current refactor reviewed whether `repository.Repositories` should stay as record-of-functions, flatten to direct SQLite calls, or move to variant-based backend dispatch.
11+
12+
Today project has one production backend: SQLite. Existing tests and runtime wiring already depend on repository records. Refactor priority for this pass is session/runtime correctness, config structure, and observability — not backend architecture churn.
13+
14+
## Decision
15+
Keep record-of-functions repository pattern as-is for now.
16+
17+
## Why
18+
- No second backend currently planned for immediate use.
19+
- Refactor churn here would be high relative to benefit.
20+
- Current abstraction already supports test seams and commit batching used by session flows.
21+
- Flattening to direct module calls would tightly couple runtime code to SQLite right before large session refactors.
22+
- Variant-based backend dispatch can be revisited later if a real in-memory or alternate backend becomes necessary.
23+
24+
## Consequences
25+
- No code change required beyond documenting decision.
26+
- Continue using `repository.Repositories` in runtime code.
27+
- Revisit only when one of these becomes true:
28+
- second backend is actively needed,
29+
- repository indirection blocks maintainability,
30+
- tests need cheaper backend swapping than current approach allows.
File renamed without changes.

docs/session-runtime.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ Capabilities:
1111
- load session snapshot from repositories on start
1212
- persist session-level `active_run_id` pointer and use it on reload
1313
- one-time fallback migration from persisted non-terminal runs when older data lacks the pointer
14-
- keep internal snapshot buffers newest-first while normalizing chronological order on public replies
14+
- keep internal snapshot buffers chronological (oldest-first) end-to-end
1515
- append inbound message with persistence
1616
- start run with single-flight guard
1717
- start or lazily rehydrate dedicated run actor for active run
@@ -32,6 +32,7 @@ Capabilities:
3232
- `tool_call.started`
3333
- `tool_call.succeeded`
3434
- `tool_call.failed`
35+
- `memory.writeback_failed`
3536

3637
### Session support
3738
`src/lummy_agent/session/support.gleam`
@@ -40,6 +41,13 @@ Capabilities:
4041
- summary fallback helper for model-backed context condensation
4142
- list replace/find helpers used by session actor state updates
4243

44+
### Session transaction pipeline
45+
`src/lummy_agent/session/session_actor.gleam`
46+
- session mutating paths now build `SessionTransaction` values
47+
- single `commit_and_apply` function persists commit batch, publishes envelopes, syncs run actor, and folds changes into snapshot
48+
- run actor syncs are explicit via `RunActorSync`
49+
- chunk events and memory writeback failures flow through same event log channel
50+
4351
### Session supervisor
4452
`src/lummy_agent/session/session_supervisor.gleam`
4553
- factory supervisor wrapper for dynamic session actors

src/lummy_agent.gleam

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,11 @@ pub fn main() -> Nil {
1212
Error(error) -> panic as app_config.render_error(error)
1313
}
1414

15-
let assert Ok(Nil) = storage_sqlite.initialize(config.database_path)
15+
let assert Ok(Nil) = storage_sqlite.initialize(config.storage.database_path)
1616
as "failed to initialize sqlite store"
1717

1818
let assert Ok(repositories) =
19-
storage_sqlite.repositories(config.database_path)
19+
storage_sqlite.repositories(config.storage.database_path)
2020
as "failed to build sqlite repositories"
2121

2222
let assert Ok(_root_supervisor) = app_supervisor.start(config, repositories)

src/lummy_agent/agent/basic_agent.gleam

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ pub fn execute(
2929
session_id: domain_id.SessionId,
3030
run: domain_run.AgentRun,
3131
) -> Result(session_actor.Snapshot, domain_error.DomainError) {
32-
let tools = tool_runtime.built_in_for_run(config, run)
32+
let tools = tool_runtime.built_in_for_run(config.tools, run)
3333
let assert Ok(initial_usage) = domain_run.usage(0, 0, 0, 0)
3434
let started_ms = clock.monotonic_ms()
3535

@@ -233,7 +233,7 @@ fn handle_tool_request(
233233
tools,
234234
tool_name,
235235
input_json,
236-
config.agent_tool_max_retries,
236+
config.tools.max_retries,
237237
)
238238
{
239239
Ok(output_json) -> {
@@ -465,7 +465,15 @@ fn fail_started_run(
465465
run_id: domain_id.RunId,
466466
reason: domain_error.DomainError,
467467
) -> Result(session_actor.Snapshot, domain_error.DomainError) {
468-
case session_manager.fail_run(manager, session_id, run_id, wall_clock_now(), reason) {
468+
case
469+
session_manager.fail_run(
470+
manager,
471+
session_id,
472+
run_id,
473+
wall_clock_now(),
474+
reason,
475+
)
476+
{
469477
Ok(snapshot) -> Ok(snapshot)
470478
Error(_) -> Error(reason)
471479
}
@@ -505,16 +513,16 @@ fn enforce_loop_budgets(
505513
usage: domain_run.Usage,
506514
started_ms: Int,
507515
) -> Result(Nil, domain_error.DomainError) {
508-
use _ <- result.try(enforce_step_budget(config.agent_max_steps, step))
516+
use _ <- result.try(enforce_step_budget(config.agent.max_steps, step))
509517
use _ <- result.try(enforce_token_budget(
510-
config.agent_max_total_tokens,
518+
config.agent.max_total_tokens,
511519
usage.total_tokens,
512520
))
513521
use _ <- result.try(enforce_cost_budget(
514-
config.agent_max_cost_micros,
522+
config.agent.max_cost_micros,
515523
usage.estimated_cost_micros,
516524
))
517-
enforce_duration_budget(config.agent_max_duration_ms, started_ms)
525+
enforce_duration_budget(config.agent.max_duration_ms, started_ms)
518526
}
519527

520528
fn enforce_step_budget(

src/lummy_agent/agent/memory_recall.gleam

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import lummy_agent/app/config as app_config
33
import lummy_agent/domain/message as domain_message
44

55
pub fn recall(
6-
config: app_config.Config,
6+
config: app_config.ToolsConfig,
77
messages: List(domain_message.ConversationMessage),
88
) -> List(memory_store.MemoryEntry) {
99
memory_store.recall(config, messages)

src/lummy_agent/agent/memory_store.gleam

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -23,19 +23,15 @@ pub type MemoryMatch {
2323
MemoryMatch(entry: MemoryEntry, score: Int, matched_terms: List(String))
2424
}
2525

26-
pub fn configured_notes_path(config: app_config.Config) -> String {
27-
case filepath.is_absolute(config.agent_tool_notes_path) {
26+
pub fn configured_notes_path(config: app_config.ToolsConfig) -> String {
27+
case filepath.is_absolute(config.notes_path) {
2828
True ->
29-
filepath.expand(config.agent_tool_notes_path)
30-
|> result.unwrap(config.agent_tool_notes_path)
29+
filepath.expand(config.notes_path) |> result.unwrap(config.notes_path)
3130

3231
False ->
33-
filepath.join(config.agent_tool_file_root, config.agent_tool_notes_path)
32+
filepath.join(config.file_root, config.notes_path)
3433
|> filepath.expand
35-
|> result.unwrap(filepath.join(
36-
config.agent_tool_file_root,
37-
config.agent_tool_notes_path,
38-
))
34+
|> result.unwrap(filepath.join(config.file_root, config.notes_path))
3935
}
4036
}
4137

@@ -78,7 +74,7 @@ pub fn entry(
7874
}
7975

8076
pub fn entries(
81-
config: app_config.Config,
77+
config: app_config.ToolsConfig,
8278
) -> Result(List(MemoryEntry), domain_error.DomainError) {
8379
let path = configured_notes_path(config)
8480

@@ -90,7 +86,7 @@ pub fn entries(
9086
}
9187

9288
pub fn append_unique_entry(
93-
config: app_config.Config,
89+
config: app_config.ToolsConfig,
9490
entry: MemoryEntry,
9591
) -> Result(Bool, domain_error.DomainError) {
9692
let path = configured_notes_path(config)
@@ -113,7 +109,7 @@ pub fn append_unique_entry(
113109
}
114110

115111
pub fn recall(
116-
config: app_config.Config,
112+
config: app_config.ToolsConfig,
117113
messages: List(domain_message.ConversationMessage),
118114
) -> List(MemoryEntry) {
119115
case latest_query(messages) {
@@ -128,7 +124,7 @@ pub fn recall(
128124
}
129125

130126
pub fn search(
131-
config: app_config.Config,
127+
config: app_config.ToolsConfig,
132128
query: String,
133129
limit: Int,
134130
) -> Result(List(MemoryMatch), domain_error.DomainError) {

0 commit comments

Comments
 (0)