samagotchi 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +43 -0
- data/LICENSE +21 -0
- data/README.md +126 -0
- data/bin/chi +1140 -0
- data/docs/architecture.md +299 -0
- data/docs/cli.md +490 -0
- data/docs/configuration.md +494 -0
- data/docs/desktop.md +97 -0
- data/docs/guardrails.md +218 -0
- data/docs/hooks.md +309 -0
- data/docs/internals/background-tasks.md +26 -0
- data/docs/internals/context-telemetry.md +36 -0
- data/docs/internals/gemma4-contract.md +23 -0
- data/docs/internals/tool-guardrails.md +45 -0
- data/docs/memory.md +85 -0
- data/docs/plugins.md +819 -0
- data/docs/releasing.md +135 -0
- data/docs/sessions.md +155 -0
- data/lib/samagotchi/bridge/bounded_queue.rb +70 -0
- data/lib/samagotchi/bridge/card_store.rb +126 -0
- data/lib/samagotchi/bridge/event_id.rb +25 -0
- data/lib/samagotchi/bridge/ring_buffer.rb +63 -0
- data/lib/samagotchi/bridge/sse_writer.rb +248 -0
- data/lib/samagotchi/bridge/turn_accumulator.rb +189 -0
- data/lib/samagotchi/bridge.rb +993 -0
- data/lib/samagotchi/bridge_client/event_stream.rb +158 -0
- data/lib/samagotchi/bridge_client/sse_parser.rb +51 -0
- data/lib/samagotchi/bridge_client.rb +330 -0
- data/lib/samagotchi/bundle_needs.rb +97 -0
- data/lib/samagotchi/bundles/btw/manifest.yml +10 -0
- data/lib/samagotchi/bundles/btw/plugin.rb +100 -0
- data/lib/samagotchi/bundles/guardrails/guardrails/rules.yml +82 -0
- data/lib/samagotchi/bundles/guardrails/guardrails.md +14 -0
- data/lib/samagotchi/bundles/guardrails/manifest.yml +8 -0
- data/lib/samagotchi/bundles/known-names/hooks/known_names.rb +210 -0
- data/lib/samagotchi/bundles/known-names/known_names.md +3 -0
- data/lib/samagotchi/bundles/known-names/manifest.yml +14 -0
- data/lib/samagotchi/bundles/loop-guard/manifest.yml +10 -0
- data/lib/samagotchi/bundles/loop-guard/plugin.rb +158 -0
- data/lib/samagotchi/bundles/mcp/manifest.yml +11 -0
- data/lib/samagotchi/bundles/mcp/plugin.rb +631 -0
- data/lib/samagotchi/bundles/system/config_modification_protocol.md +149 -0
- data/lib/samagotchi/bundles/system/delegated.md +10 -0
- data/lib/samagotchi/bundles/system/identity.md +7 -0
- data/lib/samagotchi/bundles/system/manifest.yml +11 -0
- data/lib/samagotchi/bundles/system/memory_guide.md +107 -0
- data/lib/samagotchi/bundles/system/self_map.md +55 -0
- data/lib/samagotchi/cancellation_controller.rb +78 -0
- data/lib/samagotchi/client.rb +429 -0
- data/lib/samagotchi/commands/registry.rb +112 -0
- data/lib/samagotchi/config.rb +910 -0
- data/lib/samagotchi/context_note.rb +77 -0
- data/lib/samagotchi/context_quote.rb +21 -0
- data/lib/samagotchi/context_usage.rb +66 -0
- data/lib/samagotchi/context_window.rb +76 -0
- data/lib/samagotchi/debug_log.rb +110 -0
- data/lib/samagotchi/desktop/macos/App.swift +102 -0
- data/lib/samagotchi/desktop/macos/ChiRunner.swift +201 -0
- data/lib/samagotchi/desktop/macos/Hotkey.swift +42 -0
- data/lib/samagotchi/desktop/macos/Info.plist.erb +42 -0
- data/lib/samagotchi/desktop/macos/Panel.swift +383 -0
- data/lib/samagotchi/desktop/macos.rb +255 -0
- data/lib/samagotchi/desktop.rb +21 -0
- data/lib/samagotchi/desktop_command.rb +143 -0
- data/lib/samagotchi/engine.rb +2807 -0
- data/lib/samagotchi/guardrails/approval.rb +125 -0
- data/lib/samagotchi/guardrails/approvals.rb +177 -0
- data/lib/samagotchi/guardrails/context.rb +71 -0
- data/lib/samagotchi/guardrails/gate.rb +125 -0
- data/lib/samagotchi/guardrails/load_failures.rb +46 -0
- data/lib/samagotchi/guardrails/protected_paths.rb +77 -0
- data/lib/samagotchi/guardrails/rules.rb +199 -0
- data/lib/samagotchi/guardrails/targets.rb +119 -0
- data/lib/samagotchi/guardrails/verdict.rb +134 -0
- data/lib/samagotchi/guardrails.rb +18 -0
- data/lib/samagotchi/hooks/bundle_loader.rb +158 -0
- data/lib/samagotchi/hooks/loader.rb +162 -0
- data/lib/samagotchi/hooks/registry.rb +261 -0
- data/lib/samagotchi/hooks.rb +30 -0
- data/lib/samagotchi/host_registry.rb +315 -0
- data/lib/samagotchi/idle_client.rb +147 -0
- data/lib/samagotchi/idle_recap.rb +549 -0
- data/lib/samagotchi/idle_reminders.rb +101 -0
- data/lib/samagotchi/idle_scheduler.rb +76 -0
- data/lib/samagotchi/image_store.rb +393 -0
- data/lib/samagotchi/installed_gem.rb +38 -0
- data/lib/samagotchi/kernel_loop.rb +1017 -0
- data/lib/samagotchi/launch_mode.rb +34 -0
- data/lib/samagotchi/llm/backend.rb +28 -0
- data/lib/samagotchi/llm/chat_loop.rb +450 -0
- data/lib/samagotchi/llm/errors.rb +329 -0
- data/lib/samagotchi/llm/http.rb +412 -0
- data/lib/samagotchi/llm/model_result.rb +72 -0
- data/lib/samagotchi/llm/native_backend.rb +50 -0
- data/lib/samagotchi/llm/native_tool_normalizer.rb +277 -0
- data/lib/samagotchi/llm/openai_chat.rb +403 -0
- data/lib/samagotchi/llm/usage.rb +79 -0
- data/lib/samagotchi/log.rb +200 -0
- data/lib/samagotchi/log_line.rb +127 -0
- data/lib/samagotchi/log_path.rb +31 -0
- data/lib/samagotchi/log_subscriber.rb +163 -0
- data/lib/samagotchi/memory_bundle/builder.rb +364 -0
- data/lib/samagotchi/memory_bundle/index_updater.rb +123 -0
- data/lib/samagotchi/memory_bundle/installer.rb +528 -0
- data/lib/samagotchi/memory_bundle/listing.rb +72 -0
- data/lib/samagotchi/memory_bundle/manifest.rb +225 -0
- data/lib/samagotchi/memory_bundle/merger.rb +52 -0
- data/lib/samagotchi/memory_bundle/placeholder.rb +37 -0
- data/lib/samagotchi/memory_bundle/provenance.rb +257 -0
- data/lib/samagotchi/memory_bundle/source.rb +153 -0
- data/lib/samagotchi/memory_bundle/status.rb +107 -0
- data/lib/samagotchi/memory_bundle/system_bundle.rb +161 -0
- data/lib/samagotchi/memory_bundle/uninstaller.rb +128 -0
- data/lib/samagotchi/memory_bundle.rb +17 -0
- data/lib/samagotchi/memory_paths.rb +101 -0
- data/lib/samagotchi/model_overlay.rb +53 -0
- data/lib/samagotchi/model_profile.rb +309 -0
- data/lib/samagotchi/muted_memories.rb +66 -0
- data/lib/samagotchi/note_command.rb +163 -0
- data/lib/samagotchi/output_formatter.rb +100 -0
- data/lib/samagotchi/owner_lock.rb +110 -0
- data/lib/samagotchi/pending_input_queue.rb +48 -0
- data/lib/samagotchi/plugin/api.rb +362 -0
- data/lib/samagotchi/plugin/context.rb +193 -0
- data/lib/samagotchi/plugin/loader.rb +126 -0
- data/lib/samagotchi/plugin/service.rb +117 -0
- data/lib/samagotchi/plugin/sessions.rb +150 -0
- data/lib/samagotchi/plugin/side_question.rb +60 -0
- data/lib/samagotchi/plugin/tool_result.rb +24 -0
- data/lib/samagotchi/project_scope.rb +25 -0
- data/lib/samagotchi/prompt.rb +119 -0
- data/lib/samagotchi/prompt_literal_guard.rb +70 -0
- data/lib/samagotchi/recap_store.rb +92 -0
- data/lib/samagotchi/reminder_store.rb +165 -0
- data/lib/samagotchi/self_report.rb +195 -0
- data/lib/samagotchi/send_command.rb +170 -0
- data/lib/samagotchi/served_model.rb +32 -0
- data/lib/samagotchi/session.rb +508 -0
- data/lib/samagotchi/session_commands.rb +527 -0
- data/lib/samagotchi/session_delete_command.rb +105 -0
- data/lib/samagotchi/session_manager.rb +1049 -0
- data/lib/samagotchi/session_metrics.rb +466 -0
- data/lib/samagotchi/session_observer.rb +117 -0
- data/lib/samagotchi/terminal_ui/attach_launcher.rb +118 -0
- data/lib/samagotchi/terminal_ui/attached_loop.rb +1037 -0
- data/lib/samagotchi/terminal_ui/attached_view.rb +264 -0
- data/lib/samagotchi/terminal_ui/event_renderer.rb +192 -0
- data/lib/samagotchi/terminal_ui/formatting.rb +291 -0
- data/lib/samagotchi/terminal_ui/image_input.rb +36 -0
- data/lib/samagotchi/terminal_ui/input_support.rb +324 -0
- data/lib/samagotchi/terminal_ui/legacy_surface.rb +111 -0
- data/lib/samagotchi/terminal_ui/line_reader.rb +113 -0
- data/lib/samagotchi/terminal_ui/live_region.rb +36 -0
- data/lib/samagotchi/terminal_ui/plain_surface.rb +51 -0
- data/lib/samagotchi/terminal_ui/question_prompt.rb +153 -0
- data/lib/samagotchi/terminal_ui/question_slot.rb +131 -0
- data/lib/samagotchi/terminal_ui/reline_seam.rb +216 -0
- data/lib/samagotchi/terminal_ui/repl_input.rb +138 -0
- data/lib/samagotchi/terminal_ui/screen.rb +316 -0
- data/lib/samagotchi/terminal_ui/surface.rb +47 -0
- data/lib/samagotchi/terminal_ui/thinking_line.rb +101 -0
- data/lib/samagotchi/terminal_ui.rb +1992 -0
- data/lib/samagotchi/thinking_ticker.rb +110 -0
- data/lib/samagotchi/thought_stream_splitter.rb +149 -0
- data/lib/samagotchi/token_usage.rb +88 -0
- data/lib/samagotchi/tool_activity.rb +216 -0
- data/lib/samagotchi/tool_call_parser.rb +637 -0
- data/lib/samagotchi/tool_declarations.rb +561 -0
- data/lib/samagotchi/tool_runner.rb +211 -0
- data/lib/samagotchi/tools/args.rb +259 -0
- data/lib/samagotchi/tools/ask_user_question.rb +152 -0
- data/lib/samagotchi/tools/builtins.rb +122 -0
- data/lib/samagotchi/tools/cancel_reminder.rb +21 -0
- data/lib/samagotchi/tools/delegate.rb +167 -0
- data/lib/samagotchi/tools/delegate_result.rb +53 -0
- data/lib/samagotchi/tools/delegate_wait.rb +153 -0
- data/lib/samagotchi/tools/edit.rb +155 -0
- data/lib/samagotchi/tools/execute.rb +214 -0
- data/lib/samagotchi/tools/list_reminders.rb +20 -0
- data/lib/samagotchi/tools/list_sessions.rb +74 -0
- data/lib/samagotchi/tools/memory.rb +256 -0
- data/lib/samagotchi/tools/output_guardrails.rb +93 -0
- data/lib/samagotchi/tools/peers.rb +18 -0
- data/lib/samagotchi/tools/read.rb +182 -0
- data/lib/samagotchi/tools/register_reminder.rb +53 -0
- data/lib/samagotchi/tools/registry.rb +60 -0
- data/lib/samagotchi/tools/send_note.rb +49 -0
- data/lib/samagotchi/tools/task_create.rb +29 -0
- data/lib/samagotchi/tools/task_get.rb +39 -0
- data/lib/samagotchi/tools/task_list.rb +43 -0
- data/lib/samagotchi/tools/task_runtime.rb +311 -0
- data/lib/samagotchi/tools/task_stop.rb +29 -0
- data/lib/samagotchi/tools/task_wait.rb +104 -0
- data/lib/samagotchi/tools/tool_path.rb +18 -0
- data/lib/samagotchi/tools/web_fetch.rb +163 -0
- data/lib/samagotchi/tools/write.rb +26 -0
- data/lib/samagotchi/turn_flow.rb +242 -0
- data/lib/samagotchi/turn_note.rb +76 -0
- data/lib/samagotchi/turn_tally.rb +101 -0
- data/lib/samagotchi/version.rb +7 -0
- data/lib/samagotchi/vision_context.rb +132 -0
- data/lib/samagotchi/vision_support.rb +109 -0
- data/lib/samagotchi/web/app.rb +1349 -0
- data/lib/samagotchi/web/markdown_renderer.rb +107 -0
- data/lib/samagotchi/web/message_parts.rb +169 -0
- data/lib/samagotchi/web/public/activity.js +100 -0
- data/lib/samagotchi/web/public/annotations.js +67 -0
- data/lib/samagotchi/web/public/app.js +2382 -0
- data/lib/samagotchi/web/public/card.js +74 -0
- data/lib/samagotchi/web/public/chat_view.js +360 -0
- data/lib/samagotchi/web/public/chunk_router.js +25 -0
- data/lib/samagotchi/web/public/command_complete.js +39 -0
- data/lib/samagotchi/web/public/composer_size.js +19 -0
- data/lib/samagotchi/web/public/copy.js +142 -0
- data/lib/samagotchi/web/public/ctx.js +35 -0
- data/lib/samagotchi/web/public/data.js +256 -0
- data/lib/samagotchi/web/public/format.js +232 -0
- data/lib/samagotchi/web/public/hold.js +78 -0
- data/lib/samagotchi/web/public/images.js +77 -0
- data/lib/samagotchi/web/public/index.html +568 -0
- data/lib/samagotchi/web/public/init_row.js +60 -0
- data/lib/samagotchi/web/public/model_pick.js +23 -0
- data/lib/samagotchi/web/public/question_card.js +100 -0
- data/lib/samagotchi/web/public/route.js +17 -0
- data/lib/samagotchi/web/public/scope.js +36 -0
- data/lib/samagotchi/web/public/scroll.js +24 -0
- data/lib/samagotchi/web/public/sentences.js +88 -0
- data/lib/samagotchi/web/public/sessions_list.js +60 -0
- data/lib/samagotchi/web/public/strip.js +25 -0
- data/lib/samagotchi/web/public/tally.js +37 -0
- data/lib/samagotchi/web/public/thinking_ticker.js +79 -0
- data/lib/samagotchi/web/public/timing.js +185 -0
- data/lib/samagotchi/web/public/turn_events.js +209 -0
- data/lib/samagotchi/web/public/turn_model.js +204 -0
- data/lib/samagotchi/web/public/turn_view.js +587 -0
- data/lib/samagotchi/web/server.rb +183 -0
- data/lib/samagotchi/web/session_hub.rb +329 -0
- data/lib/samagotchi/web/session_summary.rb +85 -0
- data/lib/samagotchi/worker.rb +635 -0
- data/lib/samagotchi/worker_idle_exit.rb +87 -0
- data/lib/samagotchi.rb +12 -0
- metadata +374 -0
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
# Samagotchi Architecture
|
|
2
|
+
|
|
3
|
+
A compact visual overview of the current architecture, then the core API in prose
|
|
4
|
+
(see [Core and UI](#core-and-ui)).
|
|
5
|
+
|
|
6
|
+
## System map
|
|
7
|
+
|
|
8
|
+
```
|
|
9
|
+
┌─────────────────────────────────────────┐
|
|
10
|
+
│ bin/chi │
|
|
11
|
+
│ (CLI entry point) │
|
|
12
|
+
└───────────────────┬─────────────────────┘
|
|
13
|
+
│
|
|
14
|
+
┌──────────────────────────┴──────────────┐
|
|
15
|
+
│ TerminalUI │ ← REPL (Reline),
|
|
16
|
+
│ render · status line · REPL commands │ rendering, commands
|
|
17
|
+
└──────────────────────────┬──────────────┘
|
|
18
|
+
│ delegates
|
|
19
|
+
┌───────────────────────────┴───────────────────────┐
|
|
20
|
+
│ Engine (core logic) │
|
|
21
|
+
│ system prompt · memory injection │
|
|
22
|
+
│ tool declarations · session lifecycle │
|
|
23
|
+
│ model↔tool loop — `run_turn` │
|
|
24
|
+
└───────────────────────────┬───────────────────────┘
|
|
25
|
+
│ delegates
|
|
26
|
+
┌────────────────────────────┴──────────────┐
|
|
27
|
+
│ Web::App (Rack) │ ← browser UI
|
|
28
|
+
│ session hub · /api/events · /api/* │ via SessionManager file IPC
|
|
29
|
+
└───────────────────────────┬───────────────┘
|
|
30
|
+
│
|
|
31
|
+
┌───────────────────────────────┬──────────┴───────────────┬──────────────────┐
|
|
32
|
+
│ │ │ │
|
|
33
|
+
┌─────┴─────┐ ┌───────┴───────────┐ ┌────────┴───────┐ ┌───────┴────────┐
|
|
34
|
+
│ KernelLoop │◀── drives────────▶│ Model API │ │ Session │ │ SessionManager │
|
|
35
|
+
│ (the loop)│ │ (LLM HTTP call) │ │ (state, │ │ (bg workers) │
|
|
36
|
+
└─────┬──────┘ └─────────────────────┘ │ history) │ └────────────────┘
|
|
37
|
+
│ └──────────────────┘ └────────────────┘
|
|
38
|
+
│ run_turn events (turn_started, turn_completed,
|
|
39
|
+
│ turn_canceled, raw KernelLoop events)
|
|
40
|
+
▼
|
|
41
|
+
┌──────────────────────────────────────────────────────────────────────────────────┐
|
|
42
|
+
│ Tools │
|
|
43
|
+
│ execute · read · edit · write · memory · web_fetch · task_create/get/list/ │
|
|
44
|
+
│ task_stop/wait │
|
|
45
|
+
│ declared via tool_declarations.rb │
|
|
46
|
+
└──────────────────────────────────────────────────────────────────────────────────┘
|
|
47
|
+
│
|
|
48
|
+
▼
|
|
49
|
+
┌──────────────────────────────────────────────────────────────────────────────────┐
|
|
50
|
+
│ Persistence │
|
|
51
|
+
│ Project: ~/.config/samagotchi/memories/projects/<repo>_<hash>/ (per git repo) │
|
|
52
|
+
│ System: ~/.config/samagotchi/memories/ │
|
|
53
|
+
└──────────────────────────────────────────────────────────────────────────────────┘
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## The two-layer split
|
|
57
|
+
|
|
58
|
+
```
|
|
59
|
+
TerminalUI ── delegates ──▶ Engine
|
|
60
|
+
(REPL / render / commands) (pure logic, no terminal)
|
|
61
|
+
▲ │
|
|
62
|
+
└────────── on_event: ◀────────────┘
|
|
63
|
+
(turn_started, turn_completed,
|
|
64
|
+
turn_canceled, raw KernelLoop events)
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
- **`Engine`** (`lib/samagotchi/engine.rb`) owns *all* agent logic and knows nothing
|
|
68
|
+
about the terminal.
|
|
69
|
+
- **`TerminalUI`** (`lib/samagotchi/terminal_ui.rb`) owns the REPL and rendering; it
|
|
70
|
+
delegates all core work to an `Engine`.
|
|
71
|
+
- The `on_event:` seam on `run_turn` exposes raw `KernelLoop` events plus the
|
|
72
|
+
higher-level turn events, so any new UI can render without terminal coupling.
|
|
73
|
+
|
|
74
|
+
## Request / turn flow
|
|
75
|
+
|
|
76
|
+
```
|
|
77
|
+
bin/chi ─▶ TerminalUI ─▶ Engine#run_turn ─▶ KernelLoop ──┬─▶ Model API (LLM)
|
|
78
|
+
(builds) │ └─▶ Tool call(s) ─▶ Tool
|
|
79
|
+
│ │
|
|
80
|
+
└── on_event: ─▶ render update ────┘
|
|
81
|
+
(turn started / completed / canceled)
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Layers at a glance
|
|
85
|
+
|
|
86
|
+
| Layer | Class(es) | Responsibility |
|
|
87
|
+
|-------|-----------|----------------|
|
|
88
|
+
| Core | `Samagotchi::Engine` | System prompt, memory injection, tool declarations, session lifecycle, model↔tool loop (`run_turn`). No terminal coupling. |
|
|
89
|
+
| UI | `Samagotchi::TerminalUI` | REPL (Reline), rendering, REPL commands. Delegates all core work to an `Engine`. |
|
|
90
|
+
| Model loops | `KernelLoop` (via `LLM::NativeBackend`), `LLM::ChatLoop` | The model↔tool loop: raw prompt or OpenAI chat API, chosen per host (see below). |
|
|
91
|
+
| Adapters | `Samagotchi::Client`, `LLM::OpenAIChat`, `LLM::HTTP` | Raw-prompt servers, the OpenAI chat API, and the HTTP both share. |
|
|
92
|
+
| Tools | `lib/samagotchi/tools/*` | Execute, read, edit, write, memory, task_*, web_fetch, plus runtime/output-guardrails. |
|
|
93
|
+
| Background | `Samagotchi::SessionManager` | Builds `Engine` directly (no terminal rendering) for workers. |
|
|
94
|
+
| Web | `Samagotchi::Web::App`, `Samagotchi::Web::Server`, `Samagotchi::Web::SessionHub` | Rack+WEBrick single-port `127.0.0.1:4567` (index.html + `/api/*` + SSE). The hub is chi web's projection of the session list, pushed to every tab over `GET /api/events`. |
|
|
95
|
+
| Sessions | `Samagotchi::Session`, `SessionManager` | File `sessions/<uuid>.json` + sidecar `input/`/`output/`/`pid`; retention 14d/500, `updated_at desc`, lazy sweep. |
|
|
96
|
+
|
|
97
|
+
## Entry points
|
|
98
|
+
|
|
99
|
+
- `LaunchMode.resolve` picks the terminal's mode. By default (`session.shared: true`)
|
|
100
|
+
plain `bin/chi`, `-p` and `--resume ID` run attached, like `--shared`; `--no-shared`,
|
|
101
|
+
`session.shared: false`, `--non-interactive` and `--verbose` run the REPL. `--memory`
|
|
102
|
+
and `--mute` are session fields (`preloaded_memory_names`, `muted_memory_names`) the
|
|
103
|
+
worker reads when it builds its `Engine`; `MutedMemories` filters the prompt's index and
|
|
104
|
+
the kernel's `memory_read`.
|
|
105
|
+
- The REPL → builds `TerminalUI`. `TerminalUI#run` is the single dispatch
|
|
106
|
+
for the REPL, `-p`/`--prompt`, `--non-interactive`, and `--resume`.
|
|
107
|
+
- Attached (`bin/chi`, `--attach ID`, `--shared [--resume ID]`) → `TerminalUI::AttachLauncher`: no `Engine`
|
|
108
|
+
and no `OwnerLock`; finds or starts the session's worker and runs `TerminalUI::AttachedLoop`
|
|
109
|
+
as a client of its Bridge (`BridgeClient#follow`, `post_turn`, `post_command`, `cancel`, `answer`,
|
|
110
|
+
`dismiss_question`). The worker runs in the session's `working_directory`, so `!cmd` and
|
|
111
|
+
the tools don't depend on where the terminal attached from.
|
|
112
|
+
- `bin/chi web` → builds `Web::Server` (Rack+WEBrick on `127.0.0.1:4567`, `--port`/`SAMAGOTCHI_WEB_PORT`, `--open`).
|
|
113
|
+
- `bin/chi sessions {list,stop,delete,prune,clean}` → retention & ordering (`Session.prune`, `updated_at desc`, dry-run, test-only); `stop` is `SessionManager.stop_session(wait:)`, which waits for the worker to release `owner.lock`; `delete` (`SessionDeleteCommand`) is `SessionManager.delete_session(stop:)`, which the TUI's `/exit --delete` and the web's `DELETE /api/sessions/:id` use too.
|
|
114
|
+
- `--prompt`, `--non-interactive`, and `SessionManager` workers build `Engine` directly.
|
|
115
|
+
|
|
116
|
+
## Session retention & ordering
|
|
117
|
+
|
|
118
|
+
- **Files:** `~/.local/state/samagotchi/sessions/<uuid>.json` + `<uuid>/input|output|pid|owner.lock|bridge.json` (XDG-aware).
|
|
119
|
+
- **Single owner:** the process running a session's Engine (worker or in-process TUI) holds a flock on `owner.lock` (`OwnerLock`); a second owner backs off, and the web answers 409 for a TUI-owned session.
|
|
120
|
+
- **Status:** `status` is turn state (`idle`/`running`); liveness is the lock.
|
|
121
|
+
- **Retention:** 14 days / 500 cap (env `SAMAGOTCHI_SESSION_RETENTION_DAYS`/`MAX_COUNT`, optional `KEEP_STATUS`), live-owner guard, only when `*.json` present; lazy sweep ≤1/24h from the session hub's full-probe tick (and on `GET /api/sessions`, which the page no longer calls) & `Dashboard#render_list`, manual via `bin/chi sessions prune --dry-run`.
|
|
122
|
+
- **Ordering:** `Session.list(sort:,order:,limit:,offset:)` and `GET /api/sessions?sort=&order=&limit=&offset=` default `updated_at desc`; Web UI sort/filter/pagination.
|
|
123
|
+
- **Test hygiene:** `test_run` flag when `SAMAGOTCHI_ENV=test`/`RACK_ENV=test`/`CI`, targetable via `prune --test-only` / `clean`.
|
|
124
|
+
|
|
125
|
+
## Core and UI
|
|
126
|
+
|
|
127
|
+
Samagotchi is split into a **core engine** and a **terminal UI**. The core holds all
|
|
128
|
+
agent logic and can be used without any terminal rendering; the UI is a thin layer on top.
|
|
129
|
+
|
|
130
|
+
| Layer | Class | Responsibility |
|
|
131
|
+
|-------|-------|----------------|
|
|
132
|
+
| Core | `Samagotchi::Engine` | System prompt, memory injection, tool declarations, session lifecycle, the model↔tool loop (`run_turn`). No terminal coupling. |
|
|
133
|
+
| UI | `Samagotchi::TerminalUI` | Interactive REPL (Reline), rendering (ANSI, spinner, status line), REPL commands. Delegates all core work to an `Engine`. |
|
|
134
|
+
| Model loops and adapters | `KernelLoop`, `LLM::ChatLoop`, `Samagotchi::Client`, `LLM::OpenAIChat`, `LLM::HTTP` | The model↔tool loops and the HTTP adapters they talk through (see "Model loops and adapters"). |
|
|
135
|
+
| Bridge (SSE/HTTP) | `Samagotchi::Bridge`, `SessionManager` | The **single live client transport**: an SSE read stream + HTTP POST turn/cancel/answer surface that attaches to a worker's existing `Engine` via `Engine#subscribe`. Every session worker starts it (bound `127.0.0.1`, no auth, localhost-only). |
|
|
136
|
+
| Web (Rack) | `Samagotchi::Web::App`, `SessionManager` | Single-port `127.0.0.1:4567` control plane via `rack`+`webrick` (serve `index.html` + `/api/*`; `/stream` proxies each session's Bridge). `bin/chi web` entrypoint. |
|
|
137
|
+
| Sessions | `Samagotchi::Session`, `SessionManager` | File-based `~/.local/state/samagotchi/sessions/<uuid>.json` + sidecar `input/`/`output/`/`pid`; retention (14d/500) + ordering (`updated_at desc`). |
|
|
138
|
+
|
|
139
|
+
- `bin/chi` in REPL mode (see `LaunchMode` above) builds `TerminalUI`. `TerminalUI#run` is the single
|
|
140
|
+
dispatch for the REPL, `-p`/`--prompt`, `--non-interactive`, and `--resume`: it
|
|
141
|
+
builds the working session once, runs a single prompt turn when `-p` is given,
|
|
142
|
+
then either exits (`--non-interactive`) or drops into the REPL carrying the
|
|
143
|
+
post-turn conversation.
|
|
144
|
+
- `SessionManager` background workers build `Engine` directly (no terminal rendering).
|
|
145
|
+
- `bin/chi` in attached mode (the default, `--attach`, `--shared`) builds no `Engine`: `TerminalUI::AttachLauncher` finds
|
|
146
|
+
or starts the worker, and `TerminalUI::AttachedLoop` is a client of its Bridge
|
|
147
|
+
(`BridgeClient#follow` for events, `post_turn`/`cancel`/`answer` for input). It
|
|
148
|
+
renders through the same `EventRenderer` as the REPL, on an `AttachedView` that
|
|
149
|
+
draws on a `Screen`: a live region at the bottom of the terminal (activity row,
|
|
150
|
+
prompt, status/notes/hints) under normal scrollback. From a turn's 3rd tool call the
|
|
151
|
+
activity slot gets a second, dim row: the turn's tool tally (`TurnTally`, seeded from the
|
|
152
|
+
snapshot's tool parts on a mid-turn join). Reline still reads the input,
|
|
153
|
+
but `RelineSeam` (prepended to `Reline::LineEditor`) sends its drawing to the
|
|
154
|
+
`Screen`. Without a capable terminal it falls back to `PlainSurface` (append-only).
|
|
155
|
+
|
|
156
|
+
#### Using the core
|
|
157
|
+
|
|
158
|
+
```ruby
|
|
159
|
+
engine = Samagotchi::Engine.new(mode: :assist, model_name: "gemma4", memories: [])
|
|
160
|
+
session = Samagotchi::Session.new_session(mode: "assist", model_name: "gemma4", working_directory: Dir.pwd)
|
|
161
|
+
|
|
162
|
+
engine.run_turn(session, "hello", on_event: nil) # => KernelLoop::Result (`.output`)
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
#### The `on_event` seam
|
|
166
|
+
|
|
167
|
+
`run_turn` accepts an optional `on_event:` callable that receives an event stream. It
|
|
168
|
+
forwards the raw `KernelLoop` events unchanged (the low-level contract) and adds a few
|
|
169
|
+
higher-level events so UIs get clean turn boundaries without inferring them:
|
|
170
|
+
|
|
171
|
+
- `:turn_started` — `{ session_id:, prompt: }`
|
|
172
|
+
- `:turn_completed` — `{ result: }` (the final `KernelLoop::Result`)
|
|
173
|
+
- `:turn_canceled` — `{ cancellation_reason: }`
|
|
174
|
+
|
|
175
|
+
Every event is a `Hash` with a `:type` symbol key; the sink must not raise (the Engine
|
|
176
|
+
rescues sink errors). A new UI (web, API) supplies its own `on_event` and
|
|
177
|
+
renders whatever it needs from the stream + final `Result`. The public Engine API:
|
|
178
|
+
|
|
179
|
+
```ruby
|
|
180
|
+
engine.run_turn(session, prompt, on_event: nil, max_iterations: 100, cancel_controller: nil)
|
|
181
|
+
engine.run(session: nil, prompt: "...", on_event: nil) # create/resume session + run
|
|
182
|
+
engine.system_prompt # fully built system prompt string
|
|
183
|
+
engine.session # current session (Engine owns create/resume)
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
#### Subscribing to the live stream (and the bridge)
|
|
187
|
+
|
|
188
|
+
For an **always-on** consumer (an external SSE client, a second UI), use
|
|
189
|
+
`Engine#subscribe` rather than passing `on_event:` to a single turn. It is a thread-safe,
|
|
190
|
+
error-isolated fan-out with a monotonic `event_seq` on every event:
|
|
191
|
+
|
|
192
|
+
```ruby
|
|
193
|
+
handle = engine.subscribe(observer: ->(event) { ... }) # observer receives {..., event_seq:}
|
|
194
|
+
engine.unsubscribe(handle: handle)
|
|
195
|
+
engine.session_state_snapshot # => { status:, message_count:, last_prompt:, event_seq: }
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
`Engine#subscribe` is the seam the SSE bridge (`Samagotchi::Bridge`) rides on. The bridge
|
|
199
|
+
is the **single live transport** and runs **inside the forked session worker** (the same
|
|
200
|
+
process that already owns the `Engine`); every worker starts it, and it exposes:
|
|
201
|
+
|
|
202
|
+
- `GET /session/:id/stream` — SSE stream of engine + kernel events, each with an
|
|
203
|
+
`id: <event_seq>-<epoch>` cursor (the epoch is drawn per worker's Bridge, since `event_seq` starts
|
|
204
|
+
over in each worker; snapshots and `/state` carry it as `event_id`); resume via `Last-Event-ID` /
|
|
205
|
+
`?from_seq=` (a plain `event_seq` is still accepted); a `: ping` heartbeat keeps idle proxies alive;
|
|
206
|
+
too-old reconnects, and cursors from another worker's epoch, receive a `reset` marker carrying
|
|
207
|
+
`session_state_snapshot`. `?snapshot=1` joins with a snapshot frame instead of a replay;
|
|
208
|
+
`?client_id=` names whose stream it is (`Bridge#open_streams_except`, used by `POST /exit`).
|
|
209
|
+
- `POST /session/:id/turn` — fire-and-forget turn creation; returns `202` with an `enqueued_id`
|
|
210
|
+
(delivery is at-least-once via the worker's file-IPC input path — it never calls `run_turn`
|
|
211
|
+
across the HTTP boundary). Inspect results through the read surface, not the turn response.
|
|
212
|
+
An optional `deadline` (epoch seconds; `BridgeClient` sends 5/6 of its read timeout ahead) makes
|
|
213
|
+
a request read after it (a worker frozen by sleep or SIGSTOP) answer `408 deadline_passed` and
|
|
214
|
+
not run: a client that timed out has said the message was not sent. `/answer`,
|
|
215
|
+
`/question/dismiss` and `/command` take the same `deadline` (a command is checked with the event
|
|
216
|
+
log held, as a turn is), and the Bridge logs `turn_expired`, `answer_expired`, `dismiss_expired`
|
|
217
|
+
or `command_expired`. The web app answers either kind of timeout with `504 worker_timeout`
|
|
218
|
+
("… so the command was not run"). `/cancel`, `/recap` and `/exit` take none.
|
|
219
|
+
- `POST /session/:id/cancel` — cancel the running turn; `202`, or `409` when none runs.
|
|
220
|
+
- `POST /session/:id/answer` — answer the pending question; `200`, `409` when another client
|
|
221
|
+
answered first or it is gone, `400` for an invalid selection.
|
|
222
|
+
- `POST /session/:id/question/dismiss` — leave the question unanswered (an approval: denied);
|
|
223
|
+
`200`, or `409` when it is no longer pending.
|
|
224
|
+
- `POST /session/:id/command` — a session command (`/model`, `/models`, `!rollback`, `!cmd`,
|
|
225
|
+
`/continue`) for the worker loop; `202` with a `command_id` its `:command_ran` names, `400` when
|
|
226
|
+
the line isn't one.
|
|
227
|
+
- `POST /session/:id/exit` — ask the worker to exit now (`{client_id:}`). The worker checks with
|
|
228
|
+
the event log held (`WorkerIdleExit#hold_for_request`): `200 {status: "exiting"}` and it leaves
|
|
229
|
+
like an idle exit, or `409 {status: "held", reason:}` with `turn_running`, `input_queued`,
|
|
230
|
+
`continue_offered`, `client_connected` (a stream not named by the asker), `reminders` or
|
|
231
|
+
`starting`.
|
|
232
|
+
- `GET /session/:id/state` — `session_state_snapshot` (JSON).
|
|
233
|
+
- `GET /session/:id/stats` — `Engine#stats_snapshot` for attached `/stats`: the metrics, with the context window and prompt profile asked from the server before the first turn.
|
|
234
|
+
- `GET /session/:id/snapshot` — the snapshot frame's content as one request (the web server renders
|
|
235
|
+
the messages itself, then streams from its `event_seq`).
|
|
236
|
+
- `OPTIONS *` — CORS preflight (`Access-Control-Allow-Origin: *`).
|
|
237
|
+
|
|
238
|
+
The per-session port is OS-assigned (bound to `0`) and published to a `bridge.json` sidecar
|
|
239
|
+
for client discovery. `chi web`'s `GET /api/sessions/:id/stream` proxies this bridge
|
|
240
|
+
(503 `not_live` when the worker is not running; full history of any session is served by
|
|
241
|
+
`GET /api/sessions/:id/output`). Resume/ring-buffer state is **in-memory** (v1) — durable
|
|
242
|
+
cross-process resume is a staged next step, not part of v1.
|
|
243
|
+
|
|
244
|
+
**Session hub.** The cross-session layer (which sessions exist, who owns them, what changed)
|
|
245
|
+
never pulls from the page: `chi web` runs one `Samagotchi::Web::SessionHub` (a thread inside
|
|
246
|
+
the server, no daemon) that keeps an in-memory projection of the session list and pushes
|
|
247
|
+
changes to every open tab over `GET /api/events` (SSE: a `snapshot` frame on every connect,
|
|
248
|
+
then `session` for an upsert and `session_gone` for a removal, `: ping` while idle, no replay).
|
|
249
|
+
Files stay the source of truth and workers don't know the hub. Its watcher is a 1 s tick that
|
|
250
|
+
stats the sessions dir (every session writer goes tmp + rename, which bumps the dir's mtime) and
|
|
251
|
+
each `<id>/` folder (recap.json, bridge.json), re-parsing only the files whose mtime or size
|
|
252
|
+
moved through `Session.summary_from_file`. Liveness is probed, since a killed owner leaves no
|
|
253
|
+
file trace: the owner lock every tick for the sessions the projection believes owned, and every
|
|
254
|
+
session every 10 s, so a `kill -9` shows within a second. The summary (`Web::SessionSummary`,
|
|
255
|
+
shared with `/api/sessions` and the session view) carries `owner`, `project_root` and
|
|
256
|
+
`bridge_up` (the sidecar is there *and* the lock is held: the page attaches its stream on it).
|
|
257
|
+
`POST/DELETE /api/sessions` and `/stop` rescan the session before answering (`SessionHub#touch`).
|
|
258
|
+
Without a hub (`App.new` alone) `/api/events` answers `503 no_hub` and the page falls back to
|
|
259
|
+
fetching the list.
|
|
260
|
+
|
|
261
|
+
## Model loops and adapters
|
|
262
|
+
|
|
263
|
+
Engine picks the loop from the effective model's host (`HostRegistry#resolve`):
|
|
264
|
+
|
|
265
|
+
| Loop | Class | Host | Talks through |
|
|
266
|
+
|---|---|---|---|
|
|
267
|
+
| Raw prompt | `KernelLoop`, wrapped by `LLM::NativeBackend` | no `api:`, or `llama_cpp`/`mlx`/`omlx` | `Client` (`/completion` or `/v1/completions`), chi's own Gemma/Qwen prompt and tool-call parsing |
|
|
268
|
+
| Chat | `LLM::ChatLoop` | `api: openai` | `LLM::OpenAIChat` (`/v1/chat/completions`, streamed, native tool calls) |
|
|
269
|
+
|
|
270
|
+
Both return an `LLM::ModelResult` and emit the same stream events; tool calls in
|
|
271
|
+
both go through `ToolRunner` (events, hooks, veto, output cap) and
|
|
272
|
+
`KernelLoop#dispatch_tool_call`. The chat loop's `generation_chunk` carries
|
|
273
|
+
`thinking:` (the server's `reasoning_content`), `text:` and `content:` (both);
|
|
274
|
+
its model turns keep their `tool_calls` and tool results their `tool_call_id`,
|
|
275
|
+
so later requests and resumed sessions pair them. It has its own system prompt
|
|
276
|
+
(no raw-prompt tool declarations; the tools go as JSON schemas).
|
|
277
|
+
|
|
278
|
+
**Adapters.** `Client` (raw-prompt servers) and `LLM::OpenAIChat` (one per host,
|
|
279
|
+
`HostRegistry#adapter_for`) share `LLM::HTTP`: timeouts, TLS for https, a line
|
|
280
|
+
reader for streamed bodies, the retry loop (`retry.*`; network errors, 429 and
|
|
281
|
+
500/502/503/504/529, honouring `Retry-After`; never after a stream has produced
|
|
282
|
+
output) and cancel. Cancel closes the in-flight socket from the
|
|
283
|
+
`CancellationController` listener, so it works on any thread.
|
|
284
|
+
|
|
285
|
+
**Errors.** A failed request raises an `LLM::ProviderError` of one kind:
|
|
286
|
+
`ConnectionError` (`RetryExhausted`), `RateLimited`, `ServerError`, `AuthError`,
|
|
287
|
+
`BadRequest` (`context_overflow?`) or `ProtocolError`. Engine keeps the turn's
|
|
288
|
+
conversation (the prompt plus completed tool iterations) and emits
|
|
289
|
+
`:turn_failed` with `error_kind:`, `retryable:`, `host:` and a one-line
|
|
290
|
+
`summary:`, which the REPL, the attached TUI and the web show.
|
|
291
|
+
|
|
292
|
+
**Usage and models.** `ModelResult#usage` is an `LLM::Usage` (server counts, else
|
|
293
|
+
a chars/4 estimate, else zeros; never nil). Model lists are `LLM::ModelInfo`
|
|
294
|
+
(`HostRegistry#list_all_models`); the context window comes from the running
|
|
295
|
+
server (`/props`), then the host's model list, then config.
|
|
296
|
+
|
|
297
|
+
**Keys.** A host's API key comes only from the environment variable its
|
|
298
|
+
`api_key_env:` names; it never reaches config.yml, `HOSTS_JSON`, `chi self`,
|
|
299
|
+
logs or events.
|