mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-09-15 12:42:21 +02:00
Squash Odysseus development history
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
# Agent turn contract
|
||||
|
||||
Scope: product Agent turns on 7011. Environment-owned native/TUI bridges retain
|
||||
their existing execution contract. No model weights or training settings change.
|
||||
|
||||
## Boundaries
|
||||
|
||||
1. `src/turn_contract.py` classifies capabilities, including explicit compound
|
||||
requests and referential follow-ups. Classification is selection, not permission.
|
||||
2. `routes/chat_routes.py` resolves toggles, privileges, global/plan/incognito
|
||||
restrictions, fixture restrictions and available schema inventory before
|
||||
freezing the offered set. Web enabled alone does not select web tools.
|
||||
3. `TurnContract` checks `required <= offered <= executable`, stores immutable
|
||||
serialized schema copies, and records unavailable requirements. An unavailable
|
||||
request stops without inference or substitution; unknown actions ask for clarity.
|
||||
Exact account-discovery requests narrow selection to account metadata only;
|
||||
compounds retain their declared family scope. Media operations declare their
|
||||
existing tool dependencies rather than falling back to shell generation.
|
||||
4. The agent's prompt/schema route and fallback use that same logical scope.
|
||||
Native versus textual serialization remains model-specific. Answer-only phases
|
||||
can suppress tool calls without granting a different scope.
|
||||
Contract turns preserve the already-compacted conversation and tool-call/result
|
||||
IDs. The standalone specialist prompt's latest-message-only behavior is not used
|
||||
for these product turns. Prompt domains also come from the contract.
|
||||
Accepted in-scope calls retain their model-provided arguments and native IDs;
|
||||
the explicit-intent fallback must not overwrite them with the whole user turn.
|
||||
5. The context-bound dispatcher checks membership **and** existing runtime policy,
|
||||
owner restrictions and exact-action approvals. A contract is not authorization
|
||||
to bypass those gates. Contract work bypasses terminating legacy shortcuts.
|
||||
6. `_AgentRenderState` explicitly identifies streamed versus canonical output.
|
||||
Later synthesis transfers ownership with turn-scoped replacement. The frontend
|
||||
reconciles visible DOM, not just accumulated strings; tool evidence is retained.
|
||||
Ownership is included in saved metrics and `message_saved` events.
|
||||
History and resume honor replacement scope. Single-capability turns retain
|
||||
canonical output: an always-synthesize trial caused a live notes loop and was
|
||||
reverted. Compound turns cannot terminate after only one capability's result.
|
||||
|
||||
## Verification
|
||||
|
||||
Use the project's configured Python environment, not an unrelated system Python:
|
||||
|
||||
```sh
|
||||
/home/pewds/odysseus-cookbook-fresh/.venv/bin/python -m pytest -q \
|
||||
tests/test_turn_contract.py tests/test_turn_contract_integration.py \
|
||||
tests/test_agent_turn_contract_boundaries.py tests/test_turn_rendering_js.py \
|
||||
tests/test_contract_prompt_conversation.py tests/test_product_turn_contract_route.py \
|
||||
tests/test_contract_explicit_fallback.py \
|
||||
tests/test_history_resume_rendering_js.py \
|
||||
tests/test_chat_route_tool_policy.py tests/test_tool_policy.py \
|
||||
tests/test_frontend_module_version_parity.py
|
||||
node scripts/verify_agent_turn_contract.mjs --max-turns 80 --total-ms 900000
|
||||
```
|
||||
|
||||
The browser verifier uses `sft_alex_creator` and actual 7011 Agent controls. It
|
||||
captures request toggles, SSE contract/tool events, visible output and persisted
|
||||
history. Ten families have four initial/follow-up Web-toggle combinations.
|
||||
Blocked or unrun cases are not passes. Email requires verified fixture isolation;
|
||||
do not enable global fixture mode on the user's live service to make a test pass.
|
||||
|
||||
## Remaining limits
|
||||
|
||||
- Classification is deterministic and vocabulary-based, not a proof of semantic
|
||||
understanding. Add independent behavior examples for confirmed misses.
|
||||
- Schema registration and policy permission do not guarantee a remote provider
|
||||
stays healthy throughout a turn. Runtime failure must remain visible.
|
||||
- Separate tool/argument errors, tool-service failures, rendering failures and
|
||||
verifier defects in reports. Do not infer model accuracy from routing alone.
|
||||
- Canonical summaries can still ignore presentation constraints such as a
|
||||
requested item count. Do not count those as full functional passes. Forcing an
|
||||
extra model round is not a validated general repair for this deployed model.
|
||||
- Keep all imports of a local JS module on the same URL identity. Distinct query
|
||||
versions instantiate separate module state even when source files are identical.
|
||||
|
||||
Live baseline and current matrix results are in `reports/agent-turn-contract-*`.
|
||||
The implementation is not a claim that every family has passed live verification.
|
||||
@@ -0,0 +1,55 @@
|
||||
# Background research → originating chat
|
||||
|
||||
Chat `trigger_research` calls carry a **dispatcher-supplied** `origin_chat_id`.
|
||||
The research start route verifies chat ownership before registering a durable
|
||||
`background_tool_jobs` row and starting the existing research service. Panel
|
||||
jobs have no origin and never inject a chat reply.
|
||||
|
||||
- Chat default: **2 rounds**, 120-second *soft* research budget. Explicit
|
||||
deeper/Auto rounds regain the normal research time budget. Panel defaults
|
||||
remain unchanged. This is not a guaranteed two-minute wall-clock deadline.
|
||||
- A completion callback stores the report and sources. A startup worker also
|
||||
reconciles missed callbacks and research errors/restarts.
|
||||
- When the origin has no active foreground/detached run, its model summarizes
|
||||
the report with thinking off and no tools. An outer 75-second deadline also
|
||||
bounds model-slot waits. If synthesis is unavailable, deliver an honest
|
||||
notice plus the report link; preserve the evidence for follow-ups.
|
||||
- Message and delivery marker commit in one transaction with a deterministic
|
||||
message ID. Report context is stored in server message metadata and injected
|
||||
as untrusted evidence in regular and compact model history. Long excerpts
|
||||
are explicitly marked; the saved full research report remains accessible.
|
||||
- The browser polls owner-scoped `/api/research/chat-jobs/{chat_id}`, appending
|
||||
unseen message IDs only when that chat is current and not streaming. No
|
||||
transcript replacement or forced navigation. Reloaded history deduplicates.
|
||||
- Chat uses the existing agent-thread rail and expandable rows. The compact
|
||||
header shows status and a right-aligned BG task label with the shared whirlpool
|
||||
while running; expanding reveals topic, phase/round, source count and report
|
||||
link. Rows update in place, preserving expansion/focus while chat streams.
|
||||
Completed rows remain visible; zero-source runs show a warning, not success.
|
||||
Progress polling excludes reports and internal fields.
|
||||
|
||||
Other tools are **not automatically backgrounded**. The durable handoff can be
|
||||
reused, but each future producer needs explicit launch/result/permission wiring.
|
||||
|
||||
## Verification
|
||||
|
||||
```sh
|
||||
/home/pewds/odysseus-cookbook-fresh/.venv/bin/pytest -q tests/test_background_tool_jobs.py tests/test_research_chat_runtime.py
|
||||
node --test tests/backgroundToolJobs.test.mjs
|
||||
node scripts/verify_background_delivery_isolation.mjs
|
||||
node scripts/verify_background_research_cards.mjs
|
||||
node scripts/verify_background_research_chat.mjs
|
||||
```
|
||||
|
||||
The last script uses disposable `sft_alex_creator` chats and real research/model
|
||||
calls, then removes only its own reports/chats. Do not use real-user mutations.
|
||||
It checks two-round launch, continued chat, automatic arrival, no transcript
|
||||
rebuild/duplicates, reload, and a follow-up. Inspect retained report excerpts
|
||||
and generated summary when it fails; do not equate job launch with good research.
|
||||
|
||||
Initial live runs verified delivery/navigation/follow-ups but exposed a summary
|
||||
attempt-count bug (fixed: helper requires **1 attempt**, not `max_retries=0`).
|
||||
A later full run was interrupted by an inference endpoint outage. The corrected
|
||||
summary path separately passed a real-model evidence/limitations/citation probe.
|
||||
All targeted Python tests passed (441); real DOM isolation checks passed. A clean
|
||||
full live run with useful retrieved evidence remains to be recorded.
|
||||
@@ -0,0 +1,99 @@
|
||||
# No-RAG clean loop: first diagnostic
|
||||
|
||||
## Setup
|
||||
|
||||
No live UI, service configuration, or weights changed. The standalone loop sends
|
||||
conversation history, native assistant calls and matching tool results directly
|
||||
to the served pre-Heretic model. It never rewrites queries, invents calls, swaps
|
||||
families, or strips output. Invalid calls return errors. Six executions per turn
|
||||
and seven model rounds bound the test.
|
||||
|
||||
Both arms use temperature 0, thinking disabled, 768 output tokens, and the
|
||||
original tool-work evaluator's `tools_for_mode(..., 'compact_contract_v3')`.
|
||||
This matters: the app's plain compact scrubber deletes descriptions, whereas v3
|
||||
retains empirically tested micro-hints. Previous plain-compact tests were not
|
||||
exact reproductions of the passing benchmark setup.
|
||||
|
||||
The 76 tools come from the current app's ten-family inventory, transformed by
|
||||
the original v3 builder. This is not a byte-identical frozen 99-tool benchmark
|
||||
inventory or proof of training-data identity. The report records schema and
|
||||
builder hashes. No schemas are invented for this experiment.
|
||||
|
||||
- **Stable:** same compact inventory on every turn, irrespective of spelling.
|
||||
- **Routed:** same loop, but existing `requested_capabilities` chooses inventory
|
||||
each turn. This isolates that selector; it is not the complete production RAG
|
||||
or Agent UI path. Other production normalizers are absent in both arms.
|
||||
- Private records are synthetic. No real private dispatcher is imported.
|
||||
Only fixture reads and optional public SearXNG calls execute. Other operations
|
||||
return explicit errors, so this does not validate their functionality.
|
||||
- Live search sends the exact model query to local SearXNG Bing/Yep, bypassing
|
||||
app query rewriting/filtering. Source results may vary between arms.
|
||||
|
||||
## Observations, not a blind score
|
||||
|
||||
| Case | Stable compact inventory | Selector arm |
|
||||
|---|---|---|
|
||||
| `whats the current stock mraket` | Selected `web_search`, query `current stock market` | Offered zero tools; declined live lookup |
|
||||
| Exact seeded failed exchange, then `can you look up` | Searched with corrected query | Also searched with corrected query |
|
||||
| Summarize search, explicitly no tools | Answered without tools or permission failure | Same |
|
||||
| Calendar → email → calendar | Recalled second event at 14:30 | Same |
|
||||
| Notes → second note → what does it say | Correct `view` ID and content | Same after fixture correction |
|
||||
| Deliberately irrelevant search result | Did not automatically retry | Did not automatically retry |
|
||||
| User asks for a better source | Refined and executed another search | Proposed search was not offered and was rejected |
|
||||
| Web-disabled lookup | Attempted network access via bash; sandbox rejected it | Invented unsupported current market news without tools |
|
||||
|
||||
The initial stable stock answer listed sources, not current index values. It
|
||||
does not establish that the market question was fully answered. Its subsequent
|
||||
`can you look up` elicited clarification after it had already searched. The
|
||||
separate seeded replay removes that differing-history confound.
|
||||
|
||||
The first notes fixture incorrectly accepted `get/read`, not the real `view`
|
||||
action. Both models selected the correct action, but the fixture rejected it.
|
||||
Those six original turns are invalid for execution comparison. A corrected
|
||||
six-turn rerun succeeded in both arms; the failed evidence is retained.
|
||||
|
||||
Web-off results are a release blocker: removing named web tools alone does not
|
||||
enforce network denial across general-purpose tools. The fixture prevented real
|
||||
execution, but any UI integration must use the real cross-tool permissions and
|
||||
clearly communicate unavailable capabilities. Neither arm is ready for a live
|
||||
switch. Source recovery and grounded completion also remain weak.
|
||||
|
||||
## What this changes
|
||||
|
||||
There is direct evidence that the selector can withhold needed tools, and that
|
||||
the model can repair the misspelled query itself when offered the tool. Clean
|
||||
history also supports the tested topic switches without synthetic substitutions.
|
||||
This supports continuing the clean-path experiment, not retraining or declaring
|
||||
the UI fixed. Full inventory is slower in these requests; overlapping runs and
|
||||
different source content prevent a controlled latency conclusion.
|
||||
|
||||
Next: integrate the clean loop behind a test-only UI profile with real permission
|
||||
enforcement and one renderer, preserving the v3 contract. Test live read-only
|
||||
follow-ups and explicit Web-off behavior before any rollout. Separately compare
|
||||
a generic evidence-check/retry instruction on the weak-result fixture; do not
|
||||
manufacture a retry query in the harness.
|
||||
|
||||
## Reproduce
|
||||
|
||||
Eight boundary tests pass:
|
||||
|
||||
```sh
|
||||
/home/pewds/odysseus-cookbook-fresh/.venv/bin/pytest -q tests/test_clean_tool_loop.py
|
||||
```
|
||||
|
||||
Run with a fresh report filename (existing evidence is never overwritten):
|
||||
|
||||
```sh
|
||||
/home/pewds/odysseus-cookbook-fresh/.venv/bin/python scripts/test_clean_tool_loop.py --live-search --report reports/clean-loop-v3-new-run.json
|
||||
```
|
||||
|
||||
Evidence:
|
||||
|
||||
- `reports/clean-loop-v3-20260909.json`: original 24 turns; notes fixture caveat above.
|
||||
- `reports/clean-loop-v3-stock-seeded-20260909.json`: four matched seeded follow-up turns.
|
||||
- `reports/clean-loop-v3-notes-fixture-corrected-20260909.json`: corrected six notes turns.
|
||||
|
||||
Each report retains model requests, responses, offered inventory and execution
|
||||
results. The `completed` status means the request loop finished, **not** that
|
||||
the answer passed functional evaluation. These are synthetic/public traces, not
|
||||
private user conversations. This test does not measure UI rendering or streaming.
|
||||
@@ -0,0 +1,220 @@
|
||||
# Tools v3 — No-RAG preview
|
||||
|
||||
Select this endpoint in the 7011 model picker, with model
|
||||
`odysseus-qwen3.5-tools-pre-heretic`. This endpoint owns its complete tool loop
|
||||
and enters Agent mode server-side on every turn, including ambiguous follow-ups;
|
||||
it does not depend on the legacy per-message intent classifier. Start a new chat
|
||||
for an uncontaminated comparison. Enable Web for searches. Clean routing is
|
||||
owned by the exact model identity, so both the normal `preheret` endpoint and
|
||||
the `cleanv3` alias use this runtime. Every other model remains on legacy RAG.
|
||||
|
||||
Endpoint ID: `cleanv3`. Its base URL uses the same inference server's Tailscale
|
||||
DNS name, `http://odysseus.tailb895f4.ts.net:18182/v1`, to distinguish it from
|
||||
the original IP-address route when existing chats omit endpoint IDs.
|
||||
|
||||
## Implementation
|
||||
|
||||
- `src/clean_agent_preview.py` is a separate streamed native-tool loop, entered
|
||||
before legacy routing and substitutions. It uses real authenticated tool
|
||||
dispatch, the tool-work `compact_contract_v5` builder, temperature 0,
|
||||
and thinking disabled. No weights change or inference server was started.
|
||||
- The offered tool inventory is stable except for permissions/toggles. Safe,
|
||||
explicit personal creates/updates are enabled for notes, tasks, calendar,
|
||||
memory, skills and documents. Destructive operations, shell/code, outbound
|
||||
email, browser interaction, deployment/admin changes and unrelated-family
|
||||
write substitution remain blocked. No tool or argument substitution is
|
||||
applied by the loop.
|
||||
- Native calls and matching results persist in `clean_v3_turn` metadata so
|
||||
follow-ups use actual evidence. History retains at most eight complete turns,
|
||||
trimming oldest whole turns for size; individual outputs cap at 8000 chars.
|
||||
- Real search still uses the existing search backend and its provider handling;
|
||||
this does not claim that provider quality or every backend transform is fixed.
|
||||
- All routing, privileges and default settings outside this exact Odysseus model
|
||||
remain unchanged. The loop has six execution/eight-round limits.
|
||||
- Write completion is evidence-bound: affirmative success text is replaced
|
||||
unless a private-write tool succeeded during the turn. Proposed call batches
|
||||
are policy-preflighted atomically, so a batch containing a blocked operation
|
||||
cannot partially execute before denial.
|
||||
|
||||
## Verification
|
||||
|
||||
399 focused Python tests passed after route integration. Browser runs r1/r2
|
||||
accidentally exercised the old loop and are not preview evidence. The runner
|
||||
now explicitly asserts `selection_mode=clean_compact_v3_preview`.
|
||||
|
||||
`reports/clean-v3-live-ui-r3-20260909.json` confirms the preview route, real notes
|
||||
execution, correct repetition from history, successful search and no-tool
|
||||
summary, plus visible incremental growth. Its notes assertions were for the
|
||||
old routed contract: they prohibited offering web tools even with Web enabled,
|
||||
and required another notes call for a verbatim repeat. The updated preview
|
||||
checks permit stable offers and accept an exact match to the preceding saved
|
||||
answer without re-execution; execution permissions are still asserted.
|
||||
|
||||
`reports/clean-v3-live-ui-r4-20260909.json` is the corrected four-turn check,
|
||||
including notes with Web off and search with Web on: **4/4 passed**, with the
|
||||
preview selection mode explicitly confirmed on every turn.
|
||||
These are UI smoke tests, not all-family or factual-answer benchmark scores.
|
||||
|
||||
## Disable
|
||||
|
||||
Disabling only endpoint `cleanv3` removes the duplicate picker alias; it does
|
||||
not disable this model-owned runtime. To roll back the runtime, revert the exact
|
||||
model route in `routes/chat_routes.py`. Do not delete weights, adapters, or user
|
||||
chats. The v3 schema builder dependency is
|
||||
`/home/pewds/odysseus-tool-work/scripts/eval_alltools_unseen_compare.py` and its
|
||||
schema-dropout helper; preserve those with this deployment.
|
||||
|
||||
## Expanded UI checks — 2026-09-09
|
||||
|
||||
24 additional turns completed through the preview: 23 automated passes and one
|
||||
checker false alarm. The Cookbook follow-up correctly shortened the previous
|
||||
six-server result to the first three requested names without another call. The
|
||||
checker required either a fresh call or a verbatim repeat; manual inspection
|
||||
confirmed the requested subset. Raw failure evidence is retained, not rescored.
|
||||
|
||||
Covered notes/misspellings/second-note selection, calendar/second-event time,
|
||||
tasks, documents, memory, skills, Cookbook listing, misspelled search, and Web
|
||||
toggle changes. Cross-family flows passed: Germany news → “whats my notes”,
|
||||
notes → “seach current stock mraket news”, and calendar → “now show my noes”.
|
||||
The model chose `current stock market news` itself. Every completed turn's
|
||||
audit confirmed the preview mode. Search source factual accuracy is not graded
|
||||
by this suite, and successful reads do not establish mutation coverage.
|
||||
|
||||
Email was separately attempted but the test guard stopped it because the
|
||||
stable offered inventory exceeded its metadata-only verified scope. Email
|
||||
therefore remains unverified in this expanded run; the guard was not weakened.
|
||||
No production code, service settings or weights changed during these tests.
|
||||
|
||||
Evidence under `reports/`:
|
||||
|
||||
- `clean-v3-broader-ui-20260909.json`: 16 turns, 15 automatic passes, Cookbook caveat.
|
||||
- `clean-v3-topic-switch-ui-20260909.json`: 6/6 passed.
|
||||
- `clean-v3-second-note-ui-20260909.json`: 2/2 passed.
|
||||
- `clean-v3-email-notes-ui-20260909.json`: blocked email attempt; notes not run in that file.
|
||||
|
||||
## Picker route fix
|
||||
|
||||
The previous tests selected sessions through the API, missing a real picker
|
||||
bug: local entries were deduplicated by model ID, hiding alternative endpoints
|
||||
with the same weights. The picker now uses endpoint+model identity for local
|
||||
routes too, displays the endpoint name, and scopes its last-picked send override
|
||||
to the current chat. `/api/sessions` returns owner-filtered endpoint identity
|
||||
for unambiguous saved URLs, so reload labels do not depend on loading the model
|
||||
catalog. Ambiguous identical URLs are not guessed.
|
||||
|
||||
The user-authorized chat `ec0683a2-015f-41d7-aa1f-34135c9640cb` was switched to
|
||||
`cleanv3` using the authenticated session PATCH API; no messages were inserted
|
||||
and no tool actions ran in that chat. Defaults and other chats were unchanged.
|
||||
|
||||
The runner's `--picker-route true` starts on the original route, clicks the
|
||||
preview in the real picker, sends a greeting, reloads the chat permalink, then
|
||||
asks for notes. Early picker/reload reports are incomplete, not passes: their
|
||||
label check exposed the unloaded-catalog issue. Focused route/picker/history
|
||||
tests: 16 passed.
|
||||
|
||||
Final picker test: `reports/clean-v3-picker-reload-r5-20260909.json`, **2/2
|
||||
passed**. Real picker click, greeting, permalink reload, and notes follow-up
|
||||
all confirmed the preview route. The label survived reload. R4 retained a
|
||||
history/DOM mismatch from sending before restored history was ready; the final
|
||||
driver explicitly waits for the saved first answer to render before sending.
|
||||
This does not claim a general fix for sending during unfinished history loading.
|
||||
|
||||
## Native image/VL status
|
||||
|
||||
The inference launcher previously set `--limit-mm-per-prompt` to zero images,
|
||||
so vLLM rejected attachments before the model saw them. The durable Odysseus
|
||||
launcher now permits up to three images per prompt; video remains disabled.
|
||||
|
||||
`reports/clean-v3-vl-live-r4-20260909.json` proves the real 7011 attachment
|
||||
path, clean compact route, object/color/spatial recognition, permalink reload,
|
||||
and ambiguous image follow-up. Those checks pass. Exact OCR of the deterministic
|
||||
`ODYSSEUS 42` heading fails in both the untouched Qwen 3.5 9B base and the
|
||||
fine-tune, so it remains a base/runtime capability limitation rather than a
|
||||
fine-tune regression or harness failure.
|
||||
|
||||
The same native path also passes JPEG and lossless WebP transport, object
|
||||
recognition, reload, and follow-up grounding. Evidence:
|
||||
`reports/clean-v3-vl-jpeg-r1-20260909.json` and
|
||||
`reports/clean-v3-vl-webp-r1-20260909.json`. Both remain `partial` only because
|
||||
the shared OCR check fails.
|
||||
|
||||
## Reversible write check
|
||||
|
||||
`scripts/verify_clean_v3_write.mjs` runs against only `sft_alex_creator`. It
|
||||
creates one UUID-named note through the real 7011 UI, verifies that exact row,
|
||||
requests a destructive bulk deletion, verifies the row still exists, and then
|
||||
deletes only its own test row through the authenticated API. The cleanup is
|
||||
verified by a 404 lookup.
|
||||
|
||||
Final evidence: `reports/clean-v3-write-ui-r8-20260909.json`, **passed**. Both
|
||||
turns reported `selection_mode=clean_compact_v3_preview`; creation executed via
|
||||
`manage_notes(action=add)`, the destructive action did not execute, and the
|
||||
canonical response was “No changes were made.” The earlier r3/r5 files are
|
||||
startup/placement failures, while r4/r6/r7 retained genuine intermediate
|
||||
harness and verifier failures; none should be interpreted as passes.
|
||||
|
||||
## Stateful, search, and email checks
|
||||
|
||||
The reversible stateful runner passes all six mutation families in one run:
|
||||
calendar, notes, tasks, documents, memory, and skills (**6/6**). Each flow
|
||||
creates a UUID-only artifact through the real Agent UI, verifies it by
|
||||
owner-scoped API, applies a noun-free correction, verifies persistence, and
|
||||
removes only that artifact. A direct database audit found zero active synthetic
|
||||
calendar, note, task, or document rows afterward.
|
||||
|
||||
The document failure was harness-owned. Compact description dropout left a
|
||||
vague free-form `command` field, error envelopes defaulted to exit code 0, and
|
||||
the clean loop dropped the active document ID. Compact v5 now exposes only
|
||||
required structured `edits`, reports errors truthfully, and executes against
|
||||
the request's explicit active document. Fresh document and combined stateful
|
||||
runs pass.
|
||||
|
||||
Search Web-toggle combinations `00`, `01`, `10`, and `11` pass **8/8** across
|
||||
two turns. A web question can no longer silently enable Bash because it says
|
||||
“official source”, and an unavailable Web capability exposes no unrelated
|
||||
fallback family. The quality suite passes **3/3**: evidence reuse without a
|
||||
second call, explicit official-page inspection with `web_fetch`, correction of
|
||||
“stock mraket” in actual search arguments, and a truthful unsupported result
|
||||
for a synthetic company.
|
||||
|
||||
Production-path email reads pass **3/3** through the running email MCP: account
|
||||
list, latest inbox list, and referential read of the first result. The report
|
||||
retains no account names, addresses, subjects, bodies, prompts, or answers.
|
||||
|
||||
Post-fix representative direct/follow-up coverage also passes for every family:
|
||||
notes/calendar 4/4, tasks/documents/memory/skills/Cookbook/search/shell 14/14,
|
||||
and email 3/3 in its privacy-preserving runner. The combined legacy verifier's
|
||||
metadata-only email guard correctly refused its broader stable inventory; that
|
||||
stopped report is not counted as a model failure.
|
||||
|
||||
Evidence:
|
||||
|
||||
- `reports/clean-v3-stateful-all-r3-20260909.json`
|
||||
- `reports/clean-v3-stateful-documents-r2-20260909.json`
|
||||
- `reports/clean-v3-search-toggle-final-r6-20260909.json`
|
||||
- `reports/clean-v3-search-quality-r3-20260909.json`
|
||||
- `reports/clean-v3-email-read-r1-20260909.json`
|
||||
- `reports/clean-v3-ten-family-tail-postfix-r1-20260909.json`
|
||||
|
||||
These checks verify routing, execution, persistence, follow-up, and selected
|
||||
answer-quality invariants. They are not yet the sealed all-action ship score.
|
||||
|
||||
## Compact v5 and corrected contract evidence
|
||||
|
||||
Compact v5 keeps the compact-v3 surface and adds only development-positive
|
||||
field hints for Email, Search/Hugging Face quant selection, and Shell/files.
|
||||
A Calendar date hint regressed development and was excluded. The Python tool now
|
||||
emits one final bare expression, REPL-style, without duplicating explicit
|
||||
`print(...)`; this turns otherwise correct computation calls into visible tool
|
||||
evidence for all models.
|
||||
|
||||
Under frozen scorer `odysseus.contract.v2.5`, development is 327/344 raw
|
||||
(95.06%) and 327/336 scorable (97.32%). Sealed blind is 311/344 raw (90.41%)
|
||||
and 311/336 scorable (92.56%), with zero reasoning leakage. Calendar, Shell,
|
||||
and Tasks remain below the 90% family ship floor, so the model is not yet a
|
||||
full benchmark ship candidate.
|
||||
|
||||
Fresh post-deploy real-UI evidence passes: stateful flows 6/6, Email 3/3,
|
||||
Search quality/recovery 3/3, private browser 3/3, and VL workflow 3/3. The
|
||||
Search check accepts a failed attempt only when a later tool succeeds and the
|
||||
final answer remains grounded.
|
||||
@@ -0,0 +1,125 @@
|
||||
# Regular-model tool compatibility
|
||||
|
||||
Last verified: 2026-09-09 through the authenticated 7011 Agent UI as
|
||||
`sft_alex_creator`.
|
||||
|
||||
This is the legacy-RAG track. The exact model
|
||||
`odysseus-qwen3.5-tools-pre-heretic` is excluded and remains on its model-owned
|
||||
clean compact runtime.
|
||||
|
||||
## Current baseline
|
||||
|
||||
| Endpoint | Model | Ten-family result | State |
|
||||
|---|---|---:|---|
|
||||
| DeepSeek | `deepseek-v4-flash` | 10/10 | passed |
|
||||
| DeepSeek | `deepseek-v4-pro` | 10/10 | passed |
|
||||
| OpenAI | `gpt-5.5` | 10/10 | passed |
|
||||
| OpenAI | `gpt-5.6-sol` | 10/10 | passed |
|
||||
| OpenAI | `gpt-5.6-terra` | 10/10 | passed |
|
||||
| OpenAI | `gpt-5.6-luna` | 10/10 | passed |
|
||||
| OpenRouter | `moonshotai/kimi-k3` | 10/10 | passed |
|
||||
| OpenRouter | `x-ai/grok-4.5` | 10/10 | passed |
|
||||
| OpenRouter | `qwen/qwen3-vl-235b-a22b-instruct` | 10/10 | passed |
|
||||
| OpenRouter | `openai/gpt-5-image` | n/a | image generation; chat tools unsupported |
|
||||
| Local `100.69.120.65:8062` | `Qwen/Qwen3.5-9B` | not run | endpoint unavailable |
|
||||
| Local `100.69.120.65:8062` | `GLM-5.3-Flash-Alis-MLX-4bit` | not run | endpoint unavailable |
|
||||
|
||||
The ten-family baseline covers one read-only functional turn each for notes,
|
||||
calendar, email accounts, tasks, documents, memory, skills, Cookbook/admin,
|
||||
web search, and shell. It verifies the legacy route, expected native tool call,
|
||||
execution result, visible UI answer, and absence of reasoning leakage. It is not
|
||||
yet a claim that every mutation/action variant, typo, or follow-up passes.
|
||||
|
||||
## Typo and follow-up profile
|
||||
|
||||
The stricter real-UI profile sends one misspelled read-only request to every
|
||||
family, followed immediately by a noun-free reference to the returned result.
|
||||
Read-only follow-ups must not call any tool; search follow-ups may either use
|
||||
the existing evidence or fetch the prior link. Across the nine chat-capable API
|
||||
models, the composited post-repair result is **178/180 turns (98.89%)**:
|
||||
|
||||
| Model | Conversation result |
|
||||
|---|---:|
|
||||
| `deepseek-v4-flash` | 20/20 |
|
||||
| `deepseek-v4-pro` | 20/20 |
|
||||
| `gpt-5.5` | 20/20 |
|
||||
| `gpt-5.6-sol` | 20/20 |
|
||||
| `gpt-5.6-terra` | 20/20 |
|
||||
| `gpt-5.6-luna` | 18/20 |
|
||||
| `moonshotai/kimi-k3` | 20/20 |
|
||||
| `x-ai/grok-4.5` | 20/20 |
|
||||
| `qwen/qwen3-vl-235b-a22b-instruct` | 20/20 |
|
||||
|
||||
Luna's only remaining family miss is a deliberately misspelled Shell request.
|
||||
The correct-spelling baseline passes. The harness does not auto-execute a shell
|
||||
command to hide that model-owned limitation.
|
||||
|
||||
The shared repair recognizes a uniquely misspelled action verb and family noun,
|
||||
then seals only declared safe private reads with immutable canonical arguments.
|
||||
This repaired Tasks/Documents/Memory and adjacent read families across providers
|
||||
without widening mutation or Shell authority. A compact native-tool instruction
|
||||
also tells regular API models to map clear typos to a currently offered tool.
|
||||
|
||||
Conversation evidence:
|
||||
|
||||
- `reports/regular-model-conversation-flash-r3-20260909.json`
|
||||
- `reports/regular-model-conversation-remaining-r1-20260909.json`
|
||||
- `reports/regular-model-conversation-repair-r1-20260909.json`
|
||||
- `reports/regular-model-conversation-shell-r1-20260909.json`
|
||||
- `reports/regular-model-conversation-qwen-repair-r1-20260909.json`
|
||||
- `reports/regular-model-conversation-qwen-tail-r1-20260909.json`
|
||||
- `reports/regular-model-conversation-qwen-search-r1-20260909.json`
|
||||
|
||||
Evidence:
|
||||
|
||||
- `reports/regular-model-tools-provider-final-r4-20260909.json` — Flash, GPT-5.5, Kimi: 30/30.
|
||||
- `reports/regular-model-tools-repair-r3-20260909.json` — Pro and Sol: 20/20; retained Qwen pre-final 9/10 miss.
|
||||
- `reports/regular-qwen-vl-full-r4-20260909.json` — Qwen-VL final family-switch run: 10/10.
|
||||
- `reports/regular-model-tools-remaining-20260909.json` — Terra, Luna, Grok: 30/30; records unavailable/unsupported models and pre-repair failures.
|
||||
- `reports/regular-model-tools-postfix-r1-20260909.json` — post-hardening
|
||||
rerun: nine chat-capable API models passed 90/90 family turns with zero model
|
||||
failures. Its overall status is non-passing only because the two configured
|
||||
local endpoints were offline; the image-only model remains unsupported.
|
||||
|
||||
## Family switch and page inspection
|
||||
|
||||
The six-turn switch/back flow covers notes → calendar → notes from prior
|
||||
evidence → web search → explicit `web_fetch` → calendar from prior evidence.
|
||||
All nine API models have a clean 6/6 reproduction (**54/54**). Kimi skipped
|
||||
search once in the retained first run and passed a fresh reproduction; that
|
||||
variability remains visible instead of being erased.
|
||||
|
||||
Evidence:
|
||||
|
||||
- `reports/regular-model-switchback-flash-r2-20260909.json`
|
||||
- `reports/regular-model-switchback-remaining-r1-20260909.json`
|
||||
- `reports/regular-model-switchback-kimi-r1-20260909.json`
|
||||
|
||||
## Repair that produced the clean baseline
|
||||
|
||||
Regular models no longer inherit up to three stale tool families into every
|
||||
explicit new request. Referential follow-ups still resolve from typed recent
|
||||
tool evidence, while explicit family switches receive the current family only.
|
||||
Safe required reads use `active_capabilities`, so stale offered context cannot
|
||||
disable their immutable operation. The stream layer also stops an exact long
|
||||
block repeated twice instead of waiting for a provider's full timeout.
|
||||
|
||||
The composer no longer treats generic words such as “source”, “system”, “app”,
|
||||
or “review” as authority to silently enable Bash. Explicit shell, terminal,
|
||||
repository, code-file, and direct coding requests retain workspace
|
||||
auto-escalation. This is a shared UI authority fix, not a model-name exception.
|
||||
|
||||
Run a bounded subset with:
|
||||
|
||||
```sh
|
||||
MODELS='deepseek-v4-flash,gpt-5.5' \
|
||||
FAMILIES='notes,calendar' WORKERS=2 \
|
||||
REPORT_PATH=reports/regular-model-check.json \
|
||||
node scripts/verify_regular_model_tools.mjs
|
||||
```
|
||||
|
||||
Set `PROFILE=conversation` to run the typo plus follow-up profile.
|
||||
|
||||
The runner discovers only enabled pinned models (visible cached local models
|
||||
when no pins exist), retains no tool outputs or private rows, and deletes only
|
||||
the exact sessions it creates.
|
||||
@@ -0,0 +1,92 @@
|
||||
# Search and compact-tool experiment — 2026-09-09
|
||||
|
||||
## Decision
|
||||
|
||||
Keep the normal routed profile on 7011. The all-tools compact experiment is
|
||||
implemented but **disabled**: direct routing success did not translate into a
|
||||
working Agent UI. Do not retrain or promote a profile on these measurements.
|
||||
|
||||
## Changes
|
||||
|
||||
- Short public-web lookups on the target model have an execution budget: two
|
||||
distinct token-normalized searches, one fetch, and up to three browser calls
|
||||
after the two searches. This bounds attempts, not just recovery prose. Existing
|
||||
permissions still apply; this does not make unavailable tools executable.
|
||||
- Failed/weak searches reach the model for evaluation and query refinement,
|
||||
instead of the earlier unconditional terminal evidence veto. Some legacy
|
||||
heuristics and official-site shortcuts remain; this is not a completed rewrite.
|
||||
- Search providers retain query/engine/date provenance. Unconfigured credentialed
|
||||
fallbacks are skipped. When SearXNG is the sole configured usable provider, Yep
|
||||
on the same instance is an additional fallback.
|
||||
- An unavailable warm-only family no longer vetoes an otherwise ordinary reply.
|
||||
- The all-tools experiment offers the trained compact inventory subject to
|
||||
permissions. It requires the exact test-owner environment flag and exact model
|
||||
match. The temporary service flag was removed after failed UI testing.
|
||||
|
||||
## Evidence and limits
|
||||
|
||||
| Measurement | Result | What it establishes |
|
||||
|---|---|---|
|
||||
| Focused Python regression suite | 401 passed | Covered policy, contract, provider and recovery-budget behavior |
|
||||
| Direct family-only compact schemas | 10/10 tool routing | Small public smoke test, not functional or blind accuracy |
|
||||
| Direct all-family compact schemas | 10/10 tool routing | Inventory did not break these first calls; roughly 3–4x slower in this run |
|
||||
| Full compact Agent UI, revision 3 | All eight turns failed one or more checks | Not suitable for activation; leaks, duplicate/incorrect rendering or missing expected calls |
|
||||
| Normal-profile final UI control | Five passed checks, two product failures, one capture error | Not accepted; suite status incomplete |
|
||||
| Clean synthetic notes tool-result continuation | Clean answer with both schema sizes | Model can continue correctly on that isolated input, not proof that UI failure is solely harness |
|
||||
|
||||
Direct probes used temperature 0; the UI target-model sampling path can cap at
|
||||
0.2. Prompts, history and tool-result serialization also differ. Match those
|
||||
before attributing UI failures to weights versus harness. Existing UI checks are
|
||||
not a grounded factual-answer benchmark. Unit tests are not UI acceptance.
|
||||
|
||||
Normal-profile control details: notes initial, both calendar turns, AI search
|
||||
initial, and history initial passed the automated checks. Notes follow-up hit a
|
||||
Playwright `Network.getResponseBody` capture error and is inconclusive. AI search
|
||||
follow-up explicitly requested a summary with no tools, but the contract still
|
||||
required `search_browser` and returned a permission failure. The history search
|
||||
follow-up failed the visible-leak check. These are separate from source relevance;
|
||||
the earlier warm-only fix did not cover classification as an active requirement.
|
||||
There is no matched pre-change control establishing a net improvement.
|
||||
|
||||
Provider isolation bypassed app relevance filters. Bing general often returned
|
||||
broad or unrelated results despite the full query. Google/Mojeek returned no
|
||||
results, DDG hit CAPTCHA, and Presearch timed out. Yep returned useful PostgreSQL
|
||||
documentation, but was weak or empty for several other questions. Engine health
|
||||
and source quality remain unresolved. Fallback cannot help when an earlier weak
|
||||
result survives filtering; there is no claim of universal relevance here.
|
||||
|
||||
## Reproduce and inspect
|
||||
|
||||
- `scripts/audit_search_pipeline.py`: raw provider comparison, no model.
|
||||
- `scripts/compare_compact_tool_inventory.py`: read-only model schema comparison;
|
||||
proposed calls are never executed.
|
||||
- `scripts/verify_agent_turn_contract.mjs`: real 7011 Agent UI and persisted-history
|
||||
checks. Use the dedicated test account; reports can contain private tool data.
|
||||
- `reports/search-provider-isolation.json`, `reports/search-yep-isolation.json`:
|
||||
public provider evidence.
|
||||
- `reports/compact-inventory-ablation.json`: direct routing probe.
|
||||
- `reports/full-compact-ui-audit-r3.json`: completed rejected UI experiment.
|
||||
Earlier experiment reports include an initialization error and an aborted run;
|
||||
do not combine them into an accuracy score.
|
||||
- `reports/routed-control-ui-audit-final.json`: normal-profile control replay;
|
||||
eight attempts, incomplete because of the capture error; failures retained.
|
||||
|
||||
Focused suite:
|
||||
|
||||
```sh
|
||||
/home/pewds/odysseus-cookbook-fresh/.venv/bin/pytest -q tests/test_turn_contract.py tests/test_turn_contract_integration.py tests/test_service_search_provider_guards.py tests/test_web_recovery_budget.py tests/test_tool_policy.py
|
||||
```
|
||||
|
||||
UI replay (read-only prompts, creates test chats):
|
||||
|
||||
```sh
|
||||
node scripts/verify_agent_turn_contract.mjs --families notes,calendar,search_ai,search_history --pairs notes:11,calendar:11,search_ai:11,search_history:11 --max-turns 8 --total-ms 360000 --turn-ms 45000 --report reports/routed-control-ui-audit-final.json
|
||||
```
|
||||
|
||||
## Next discriminating test
|
||||
|
||||
Replay the same captured UI request directly, preserving sampling, compact
|
||||
schemas, history and tool results. Then change one layer at a time. Separately
|
||||
score retrieved-source relevance and supported answers. Replace failing generic
|
||||
boundaries only when the replay identifies them; do not add rules for individual
|
||||
user phrasings or treat successful tool routing as successful execution.
|
||||
@@ -0,0 +1,73 @@
|
||||
# Typo-tolerant tool routing audit
|
||||
|
||||
The 9B SFT model was not retrained. This audit targets the earlier harness
|
||||
stage that decides which complete tool families the model is allowed to see.
|
||||
|
||||
## Method
|
||||
|
||||
- Source prompts: real `sft_alex_creator` sessions from `a37dcb3b-...` onward.
|
||||
- Labels: recorded single-family tool calls, excluding mixed/ambiguous traces.
|
||||
- Variants: deletion, adjacent transposition, duplicated character,
|
||||
keyboard-neighbor substitution, and accidental word split.
|
||||
- Split: deterministic SHA-256 assignment before scoring (75% dev, 25% blind).
|
||||
- Safety: static routing only; no historical mutation or send action is replayed.
|
||||
- Acceptance: at least 95% blind exact-family accuracy and below 1% blind
|
||||
wrong-family authorization. Abstention is measured separately.
|
||||
|
||||
## Results
|
||||
|
||||
| Router | Dev family supplied | Blind family supplied | Blind exact | Blind wrong-family |
|
||||
|---|---:|---:|---:|---:|
|
||||
| Previous exact rules | 63.64% | 65.69% | — | — |
|
||||
| Conservative fuzzy fallback r4 | 96.31% | 98.31% | 96.62% | 0.00% |
|
||||
| Final router + safe-read repair | 98.31% | 98.73% | 97.05% | 0.00% |
|
||||
|
||||
The fallback runs only for action/lookup-shaped requests, resolves exactly one
|
||||
nearby family term, and abstains on ambiguity. Conceptual questions remain
|
||||
tool-free. Complete family schemas are still selected by the immutable turn
|
||||
contract; fuzzy matching never chooses an individual tool or its arguments.
|
||||
|
||||
Authoritative machine reports:
|
||||
|
||||
- `reports/typo-tool-routing-baseline-20260909.json`
|
||||
- `reports/typo-tool-routing-fuzzy-r4-20260909.json`
|
||||
- `reports/typo-tool-routing-final-20260909.json`
|
||||
- `reports/post-followup-agent-80-20260909.json`
|
||||
- `reports/post-typo-routing-agent-80-20260909.json`
|
||||
- `reports/live-typo-agent-20-20260909.json`
|
||||
- `reports/live-typo-unresolved-r3-20260909.json`
|
||||
- `reports/live-typo-agent-final-20-20260909.json`
|
||||
- `reports/post-typo-safe-read-agent-final-80-20260909.json`
|
||||
|
||||
## Live 7011 findings
|
||||
|
||||
The post-deployment standard matrix passed 80/80 through the real Agent UI.
|
||||
The first read-only typo matrix then attempted 17 of 20 planned turns before
|
||||
its total-time limit. Initial Notes, Calendar, Email, Tasks, Documents, and
|
||||
Cookbook calls passed. Completed failing turns still had the correct family
|
||||
and required tool in `turn_contract.offered`; the 9B model sometimes answered
|
||||
without calling that offered tool. Memory and Search also exposed timeouts.
|
||||
|
||||
This separates three failure classes:
|
||||
|
||||
1. **Tool injection:** addressed by conservative fuzzy family routing; blind
|
||||
exact routing is 96.62% with zero blind wrong-family authorizations.
|
||||
2. **Required read execution:** a correctly offered safe list/refresh tool can
|
||||
still be skipped by the model, especially after a typo or on “list those
|
||||
again” follow-ups. This should be handled by the generic deterministic
|
||||
safe-read path, not additional prompt-specific hints.
|
||||
3. **Runtime timeout:** Search and one Memory follow-up require loop/backend
|
||||
diagnosis. A timeout is not counted as a model-accuracy or routing result.
|
||||
|
||||
The generic safe-read parser and search-family precedence were then repaired.
|
||||
The previously unresolved Calendar, Email, Search, and Shell/Files cases passed
|
||||
8/8. The complete typo matrix passed 20/20, including initial requests and
|
||||
follow-ups for all ten families. The final standard Agent UI compatibility
|
||||
matrix passed 80/80 across family, Web-toggle, and follow-up combinations.
|
||||
|
||||
The broad routing regression suite passed 458 tests. The model was not
|
||||
retrained and no DeepSeek API was used: the measured defect was in harness
|
||||
family selection and deterministic safe-read execution, upstream of the
|
||||
model. All 1,535 unique labeled historical turns were statically audited to
|
||||
mine failure categories. Historical write/send/delete actions were not replayed
|
||||
against live data; live verification used the deduplicated read-only matrices.
|
||||
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 3.0 MiB |
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 3.4 MiB |
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 1.1 MiB |
Binary file not shown.
Binary file not shown.
-882
@@ -1,882 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="description" content="Odysseus — a self-hosted AI workspace: chat, agents, tools, model serving, email, research, and more. Your models, your hardware, your data.">
|
||||
<title>Odysseus — A Self-Hosted AI Workspace</title>
|
||||
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Cpath d='M16 4L16 22L6 22Z' fill='%23e06c75'/%3E%3Cpath d='M16 8L16 22L24 22Z' fill='%23e06c75' opacity='0.6'/%3E%3Cpath d='M4 24Q10 20 16 24Q22 28 28 24' stroke='%23e06c75' stroke-width='2.5' fill='none' stroke-linecap='round'/%3E%3C/svg%3E">
|
||||
<style>
|
||||
:root {
|
||||
/* Odysseus default theme — exact app tokens */
|
||||
--bg: #282c34;
|
||||
--bg2: #1e2228; /* app code/hl background */
|
||||
--panel: #111; /* app panel surface */
|
||||
--panel2: #1e2228;
|
||||
--fg: #9cdef2; /* signature cyan text */
|
||||
--heading: #9cdef2;
|
||||
--muted: #6b8a94; /* app subheader */
|
||||
--border: #355a66; /* teal border */
|
||||
--accent: #e06c75; /* app accent (the send-button coral) */
|
||||
--accent2: #f0989e; /* lighter coral for gradients */
|
||||
--green: #50fa7b;
|
||||
--gold: #f0ad4e; /* app --warn */
|
||||
--red: #e06c75;
|
||||
--radius: 8px;
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
html { scroll-behavior: smooth; scroll-snap-type: y mandatory; scroll-padding-top: 60px; }
|
||||
/* Each section is a full-viewport "page" with its content centered, so only
|
||||
one shows at a time and the snap is obvious. */
|
||||
.hero, section {
|
||||
scroll-snap-align: start; min-height: 100vh;
|
||||
display: flex; flex-direction: column; justify-content: center;
|
||||
}
|
||||
/* Alternate the page backgrounds: slate (the body) ↔ black, to make each
|
||||
page boundary obvious. */
|
||||
/* Subtle dot-grid texture across the whole page. */
|
||||
section:nth-of-type(odd) {
|
||||
background-color: #111111;
|
||||
background-image: radial-gradient(circle, rgba(156,222,242,0.075) 1px, transparent 1.4px);
|
||||
background-size: 24px 24px;
|
||||
}
|
||||
section:nth-of-type(even) {
|
||||
background-color: var(--bg);
|
||||
background-image: radial-gradient(circle, rgba(156,222,242,0.06) 1px, transparent 1.4px);
|
||||
background-size: 24px 24px;
|
||||
}
|
||||
/* Customers section gets a brand-colored gradient glow over the dots. */
|
||||
#testimonials {
|
||||
background-color: var(--bg);
|
||||
background-image:
|
||||
radial-gradient(900px 520px at 80% 8%, rgba(224,108,117,0.14), transparent 60%),
|
||||
radial-gradient(760px 520px at 8% 96%, rgba(53,90,102,0.32), transparent 58%),
|
||||
radial-gradient(circle, rgba(156,222,242,0.06) 1px, transparent 1.4px);
|
||||
background-size: cover, cover, 24px 24px;
|
||||
}
|
||||
/* Domino reveal — each section fades/slides up as it scrolls into view. */
|
||||
.hero, section { opacity: 0; transform: translateY(24px); transition: opacity .6s cubic-bezier(.2,.7,.2,1), transform .6s cubic-bezier(.2,.7,.2,1); }
|
||||
.hero.in, section.in { opacity: 1; transform: none; }
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
html { scroll-snap-type: none; }
|
||||
.hero, section { opacity: 1 !important; transform: none !important; transition: none; }
|
||||
}
|
||||
/* Capabilities cards cascade in like the app's domino expand. */
|
||||
#features .feature { opacity: 0; transform: translateY(16px); }
|
||||
#features.in .feature { animation: domino-in .5s cubic-bezier(.2,.7,.2,1) forwards; }
|
||||
#features.in .feature:nth-child(1) { animation-delay: .04s; }
|
||||
#features.in .feature:nth-child(2) { animation-delay: .09s; }
|
||||
#features.in .feature:nth-child(3) { animation-delay: .14s; }
|
||||
#features.in .feature:nth-child(4) { animation-delay: .19s; }
|
||||
#features.in .feature:nth-child(5) { animation-delay: .24s; }
|
||||
#features.in .feature:nth-child(6) { animation-delay: .29s; }
|
||||
#features.in .feature:nth-child(7) { animation-delay: .34s; }
|
||||
#features.in .feature:nth-child(8) { animation-delay: .39s; }
|
||||
#features.in .feature:nth-child(9) { animation-delay: .44s; }
|
||||
@keyframes domino-in { to { opacity: 1; transform: none; } }
|
||||
body {
|
||||
margin: 0;
|
||||
background:
|
||||
radial-gradient(1100px 520px at 82% -10%, rgba(224,108,117,0.12), transparent 60%),
|
||||
radial-gradient(900px 520px at 0% 0%, rgba(53,90,102,0.30), transparent 55%),
|
||||
var(--bg);
|
||||
color: var(--fg);
|
||||
font-family: 'Fira Code', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||
line-height: 1.6;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
a { color: var(--accent); text-decoration: none; }
|
||||
.wrap { max-width: 1080px; margin: 0 auto; padding: 0 22px; }
|
||||
|
||||
/* Nav */
|
||||
nav {
|
||||
position: sticky; top: 0; z-index: 50;
|
||||
backdrop-filter: blur(10px);
|
||||
background: rgba(17,17,17,0.88);
|
||||
border-bottom: 1px solid #9cdef2;
|
||||
}
|
||||
nav .wrap { display: flex; align-items: center; justify-content: space-between; height: 60px; }
|
||||
.brand { display: flex; align-items: center; gap: 8px; font-weight: 700; font-size: 17px; letter-spacing: 0.2px; color: var(--heading); }
|
||||
.brand .boat { color: var(--accent); flex-shrink: 0; }
|
||||
.nav-links { display: flex; align-items: center; gap: 22px; }
|
||||
.nav-links a { color: var(--muted); font-size: 14px; font-weight: 500; }
|
||||
.nav-links a:hover { color: var(--fg); }
|
||||
.btn {
|
||||
display: inline-flex; align-items: center; gap: 8px;
|
||||
padding: 9px 16px; border-radius: 10px; font-weight: 600; font-size: 14px;
|
||||
border: 1px solid var(--border); color: var(--fg); background: var(--panel);
|
||||
transition: transform .12s ease, border-color .12s ease, background .12s ease;
|
||||
}
|
||||
.btn:hover { transform: translateY(-1px); border-color: var(--accent); }
|
||||
.btn.primary {
|
||||
background: linear-gradient(135deg, var(--accent), var(--accent2));
|
||||
color: #fff; border: none;
|
||||
}
|
||||
.btn.primary:hover { filter: brightness(1.07); }
|
||||
|
||||
/* Hero */
|
||||
.hero { padding: 86px 0 40px; text-align: center; position: relative; overflow: hidden; }
|
||||
#hero-flow { position: absolute; inset: 0; width: 100%; height: 100%; z-index: 0; pointer-events: none; opacity: 0.9; }
|
||||
.hero .wrap { position: relative; z-index: 2; }
|
||||
.hero h1, .hero .lede, .hero .wordmark { text-shadow: 0 2px 20px rgba(0,0,0,0.45); }
|
||||
@media (prefers-reduced-motion: reduce) { #hero-flow { display: none; } }
|
||||
.badge {
|
||||
display: inline-flex; align-items: center; gap: 7px;
|
||||
font-size: 12.5px; color: var(--muted); border: 1px solid var(--border);
|
||||
background: var(--panel); padding: 5px 12px; border-radius: 999px; margin-bottom: 22px;
|
||||
}
|
||||
.badge .dot { width: 7px; height: 7px; border-radius: 50%; background: var(--green); box-shadow: 0 0 8px var(--green); }
|
||||
.hero-logo { display: flex; align-items: center; justify-content: center; gap: 14px; color: var(--accent); margin-bottom: 4px; }
|
||||
.hero-logo svg { filter: drop-shadow(0 4px 18px rgba(224,108,117,0.35)); }
|
||||
.hero-logo .wordmark { font-size: clamp(30px, 6vw, 44px); font-weight: 700; color: var(--heading); letter-spacing: -0.01em; line-height: 1; }
|
||||
.hero h1 {
|
||||
font-size: clamp(32px, 5.4vw, 52px); line-height: 1.12; margin: 0 0 18px;
|
||||
letter-spacing: -0.01em; font-weight: 700; color: var(--heading);
|
||||
}
|
||||
.hero h1 .grad {
|
||||
background: linear-gradient(120deg, var(--accent), var(--accent2));
|
||||
-webkit-background-clip: text; background-clip: text; -webkit-text-fill-color: transparent;
|
||||
}
|
||||
.hero .slogan { font-style: italic; color: var(--accent); font-size: 12px; margin: 0 0 24px; letter-spacing: 0.3px; opacity: 0.9; }
|
||||
.hero p.lede { font-size: clamp(16px, 2.4vw, 20px); color: var(--muted); max-width: 680px; margin: 0 auto 30px; }
|
||||
.hero-cta { display: flex; gap: 12px; justify-content: center; flex-wrap: wrap; }
|
||||
|
||||
/* terminal origin card */
|
||||
.term-intro { color: var(--fg); font-size: clamp(13px, 1.8vw, 15px); margin: 34px auto 0; max-width: 560px; }
|
||||
.term {
|
||||
max-width: 620px; margin: 12px auto 0; text-align: left;
|
||||
background: var(--bg2); border: 1px solid var(--border); border-radius: var(--radius);
|
||||
overflow: hidden; box-shadow: 0 24px 60px rgba(0,0,0,0.4);
|
||||
}
|
||||
.term-bar { display: flex; align-items: center; justify-content: space-between; padding: 5px 6px 5px 12px; border-bottom: 1px solid var(--border); background: #20242c; }
|
||||
.term-bar .ttl { color: var(--muted); font-size: 12px; font-family: 'Fira Code', ui-monospace, monospace; }
|
||||
.term-bar .winbtns { display: flex; gap: 1px; }
|
||||
.term-bar .winbtns span { cursor: pointer; }
|
||||
.term { transition: opacity .18s ease, transform .18s ease; }
|
||||
/* Minimized = a rounded "pill", like the app's tab-down dock chip. */
|
||||
.term.term-min { max-width: max-content; border-radius: 999px; box-shadow: 0 6px 22px rgba(0,0,0,0.4); }
|
||||
.term.term-min .term-bar { border-bottom: none; border-radius: 999px; padding: 7px 10px 7px 16px; gap: 12px; background: var(--panel); }
|
||||
.term.term-min pre { display: none; }
|
||||
.term.term-closed { opacity: 0; transform: scale(0.96); pointer-events: none; height: 0; margin: 0 auto; border: 0; overflow: hidden; }
|
||||
.term-reopen {
|
||||
display: none; margin: 14px auto 0; font-family: 'Fira Code', monospace; font-size: 12px;
|
||||
color: var(--muted); background: none; border: 1px dashed var(--border); border-radius: 6px;
|
||||
padding: 5px 12px; cursor: pointer;
|
||||
}
|
||||
.term-reopen:hover { color: var(--accent); border-color: var(--accent); }
|
||||
.term-reopen.show { display: inline-block; }
|
||||
.term-bar .winbtns span {
|
||||
width: 28px; height: 20px; display: inline-flex; align-items: center; justify-content: center;
|
||||
border-radius: 4px; color: var(--muted); font-size: 12px; line-height: 1;
|
||||
}
|
||||
.term-bar .winbtns span:hover { background: rgba(156,222,242,0.12); color: var(--fg); }
|
||||
.term-bar .winbtns span.x:hover { background: #c0392b; color: #fff; }
|
||||
.term pre {
|
||||
margin: 0; padding: 18px 16px; font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
||||
font-size: 13.5px; color: var(--fg); line-height: 1.7; white-space: pre-wrap;
|
||||
}
|
||||
.term .cs { color: var(--green); } .term .cm { color: #828997; }
|
||||
.term-cursor { display: inline-block; color: var(--fg); font-weight: 400; animation: term-blink 1.05s steps(1) infinite; }
|
||||
@keyframes term-blink { 50% { opacity: 0; } }
|
||||
|
||||
/* Sections */
|
||||
section { padding: 60px 0; }
|
||||
.eyebrow { color: var(--accent); font-weight: 700; font-size: 12px; letter-spacing: 0.12em; text-transform: uppercase; display: inline-flex; align-items: center; gap: 6px; }
|
||||
.eyebrow svg { width: 14px; height: 14px; flex-shrink: 0; }
|
||||
h2.h { font-size: clamp(19px, 2.7vw, 26px); margin: 8px 0 12px; letter-spacing: -0.01em; color: var(--heading); font-weight: 700; }
|
||||
.sub { color: var(--muted); max-width: 620px; }
|
||||
.center { text-align: center; }
|
||||
.center .sub { margin: 0 auto; }
|
||||
|
||||
/* Testimonial gag — single featured testimonial, click/swipe to cycle (all sizes) */
|
||||
.tcarousel-wrap { position: relative; max-width: 820px; margin: 36px auto 0; }
|
||||
.tarrow {
|
||||
position: absolute; top: 50%; transform: translateY(-50%); z-index: 4;
|
||||
width: 38px; height: 38px; border-radius: 50%;
|
||||
background: rgba(17,17,17,0.85); border: 1px solid var(--border); color: var(--fg);
|
||||
font-size: 20px; line-height: 1; cursor: pointer;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
transition: border-color .12s ease, color .12s ease;
|
||||
}
|
||||
.tarrow:hover { border-color: var(--accent); color: var(--accent); }
|
||||
.tarrow.prev { left: 0; }
|
||||
.tarrow.next { right: 0; }
|
||||
.tgrid {
|
||||
display: block; position: relative; overflow: hidden; cursor: pointer;
|
||||
margin: 0 auto; max-width: 740px;
|
||||
}
|
||||
.tgrid .tcard {
|
||||
display: none;
|
||||
flex-direction: row-reverse; align-items: center; gap: 24px; text-align: left;
|
||||
background: var(--panel); border: 1px solid var(--border); border-radius: var(--radius);
|
||||
padding: 28px;
|
||||
}
|
||||
.tgrid .tcard.active { display: flex; animation: tslide .25s ease both; }
|
||||
.tgrid .tcard.active.shake { animation: tshake .5s ease-in-out 2 both; }
|
||||
.tcard .av {
|
||||
width: 84px; height: 84px; border-radius: 50%; overflow: hidden;
|
||||
border: 1px solid var(--border); background: var(--panel2); flex: 0 0 auto;
|
||||
}
|
||||
.tcard .av img, .tcard .av svg { width: 100%; height: 100%; object-fit: cover; display: block; }
|
||||
.tcard .tmeta { flex: 1 1 auto; }
|
||||
.tcard .q { font-size: 18px; color: var(--fg); margin: 0 0 12px; }
|
||||
.tcard .stars { font-size: 15px; letter-spacing: 3px; margin: 0 0 8px; color: var(--gold); }
|
||||
.tcard .stars.zero { color: var(--muted); opacity: 0.5; }
|
||||
.tcard .nm { font-weight: 700; font-size: 14.5px; }
|
||||
.tcard .rl { color: var(--muted); font-size: 12.5px; }
|
||||
.tcard.cyclops { border-color: rgba(255,90,90,0.45); background: linear-gradient(180deg, rgba(255,80,80,0.06), var(--panel)); }
|
||||
.tcard.cyclops .q { color: #ff8a8a; font-weight: 700; letter-spacing: 0.4px; word-break: break-word; }
|
||||
.tnav { display: block; text-align: center; margin-top: 18px; }
|
||||
.tdot { display: inline-block; width: 9px; height: 9px; border-radius: 50%; background: #39414d; margin: 0 4px; cursor: pointer; }
|
||||
.tdot.on { background: var(--accent); }
|
||||
.thint { font-size: 12px; color: var(--muted); margin-top: 8px; }
|
||||
@keyframes tshake {
|
||||
0%,100% { transform: translateX(0) rotate(0); }
|
||||
10% { transform: translateX(-9px) rotate(-1.5deg); }
|
||||
20% { transform: translateX(9px) rotate(1.5deg); }
|
||||
35% { transform: translateX(-7px) rotate(-1deg); }
|
||||
50% { transform: translateX(7px) rotate(1deg); }
|
||||
65% { transform: translateX(-5px); } 80% { transform: translateX(4px); } 92% { transform: translateX(-2px); }
|
||||
}
|
||||
@keyframes tslide { from { opacity: 0; transform: translateX(24px); } to { opacity: 1; transform: none; } }
|
||||
|
||||
.grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; margin-top: 36px; }
|
||||
.feature {
|
||||
background: var(--panel); border: 1px solid var(--border); border-radius: var(--radius);
|
||||
padding: 22px; transition: transform .14s ease, border-color .14s ease;
|
||||
}
|
||||
.feature:hover { transform: translateY(-3px); border-color: var(--accent); }
|
||||
.feature .ico {
|
||||
width: 40px; height: 40px; border-radius: 10px; display: inline-flex; align-items: center; justify-content: center;
|
||||
background: linear-gradient(135deg, rgba(224,108,117,0.18), rgba(53,90,102,0.28));
|
||||
border: 1px solid var(--border); color: var(--accent); margin-bottom: 14px;
|
||||
}
|
||||
.feature h3 { margin: 0 0 6px; font-size: 16.5px; }
|
||||
.feature p { margin: 0; color: var(--muted); font-size: 14px; }
|
||||
|
||||
/* Screenshot strip */
|
||||
.shotrow { display: grid; grid-template-columns: 1.4fr 1fr 1fr; gap: 16px; margin-top: 8px; }
|
||||
.shot {
|
||||
border: 1px solid var(--border); border-radius: var(--radius); overflow: hidden;
|
||||
background: linear-gradient(180deg, var(--panel), var(--panel2));
|
||||
aspect-ratio: 16/10; display: flex; align-items: center; justify-content: center;
|
||||
color: var(--muted); font-size: 13px; position: relative;
|
||||
}
|
||||
.shot .ph { display: flex; flex-direction: column; align-items: center; gap: 8px; opacity: 0.7; }
|
||||
.shot .frame-dots { position: absolute; top: 10px; left: 12px; display: flex; gap: 5px; }
|
||||
.shot .frame-dots i { width: 8px; height: 8px; border-radius: 50%; background: #39414d; display: inline-block; }
|
||||
|
||||
/* Previews — expanding hover carousel that plays a video on hover */
|
||||
.previews { display: flex; align-items: center; gap: 12px; height: 480px; max-width: 1000px; margin: 36px auto 0; }
|
||||
.preview-panel {
|
||||
position: relative; flex: 1 1 0; min-width: 0; height: 360px; overflow: hidden;
|
||||
border: 1px solid var(--border); border-radius: var(--radius); cursor: pointer;
|
||||
background: linear-gradient(180deg, var(--panel), var(--panel2));
|
||||
transition: flex-grow .5s cubic-bezier(.2,.7,.2,1), height .5s cubic-bezier(.2,.7,.2,1), border-color .25s ease;
|
||||
}
|
||||
.previews:hover .preview-panel { flex-grow: 0.55; height: 300px; }
|
||||
.preview-panel:hover, .preview-panel:focus-visible { flex-grow: 3.4 !important; height: 480px !important; border-color: var(--accent); }
|
||||
.preview-panel .ph {
|
||||
position: absolute; inset: 0; display: flex; flex-direction: column;
|
||||
align-items: center; justify-content: center; gap: 10px;
|
||||
color: var(--muted); font-size: 12.5px; opacity: 0.7; text-align: center; padding: 8px;
|
||||
}
|
||||
.preview-panel video {
|
||||
position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover;
|
||||
z-index: 1; opacity: 0; transition: opacity .3s ease; background: transparent;
|
||||
}
|
||||
.preview-panel.has-video video { opacity: 1; }
|
||||
/* These clips have their action on the left, so show the left edge instead of
|
||||
the centered crop. */
|
||||
.preview-panel:has(source[src="document.webm"]) video,
|
||||
.preview-panel:has(source[src="notes.webm"]) video { object-position: right center; }
|
||||
.preview-panel .label {
|
||||
position: absolute; z-index: 2; left: 0; right: 0; bottom: 0; padding: 14px 16px;
|
||||
background: linear-gradient(0deg, rgba(0,0,0,0.82), transparent);
|
||||
color: var(--heading);
|
||||
display: flex; flex-direction: column; align-items: flex-start; gap: 4px;
|
||||
}
|
||||
.preview-panel .label .t { display: flex; align-items: center; gap: 8px; white-space: nowrap; font-weight: 700; font-size: 14px; }
|
||||
.preview-panel .label .ico { color: var(--accent); flex-shrink: 0; }
|
||||
.preview-panel .label .desc {
|
||||
font-weight: 400; font-size: 12.5px; line-height: 1.35; color: rgba(255,255,255,0.82);
|
||||
white-space: normal; max-height: 0; opacity: 0; overflow: hidden;
|
||||
transition: max-height .4s ease, opacity .4s ease;
|
||||
}
|
||||
.preview-panel:hover .label .desc, .preview-panel:focus-visible .label .desc { max-height: 64px; opacity: 1; }
|
||||
@media (max-width: 760px) {
|
||||
.previews { flex-direction: column; height: auto; }
|
||||
.preview-panel { height: 200px; flex: none; }
|
||||
.previews:hover .preview-panel, .preview-panel:hover { flex: none !important; }
|
||||
.preview-panel .label .desc { max-height: 64px; opacity: 1; }
|
||||
}
|
||||
|
||||
/* Fullscreen video background for a section — treated as an ambient, cinematic
|
||||
backdrop (soft blur + slow drift) so it sets a mood without fighting the copy. */
|
||||
.has-bg-video { position: relative; overflow: hidden; }
|
||||
.has-bg-video .sec-bg {
|
||||
position: absolute; inset: 0; width: 100%; height: 100%;
|
||||
object-fit: cover; z-index: 0; pointer-events: none;
|
||||
/* blur softens the busy frame; the extra scale hides the blurred edges */
|
||||
filter: blur(4px) saturate(1.08) brightness(0.92);
|
||||
transform: scale(1.12);
|
||||
transform-origin: 55% 45%;
|
||||
animation: bg-drift 36s ease-in-out infinite alternate;
|
||||
will-change: transform;
|
||||
}
|
||||
@keyframes bg-drift {
|
||||
from { transform: scale(1.12) translate(0, 0); }
|
||||
to { transform: scale(1.2) translate(-2.5%, -1.5%); }
|
||||
}
|
||||
.has-bg-video .sec-bg-tint {
|
||||
position: absolute; inset: 0; z-index: 1; pointer-events: none;
|
||||
background:
|
||||
radial-gradient(900px 520px at 78% 18%, rgba(224,108,117,0.16), transparent 60%),
|
||||
radial-gradient(760px 520px at 8% 88%, rgba(53,90,102,0.30), transparent 58%),
|
||||
linear-gradient(180deg, rgba(17,17,17,0.86), rgba(17,17,17,0.62) 42%, rgba(17,17,17,0.92)),
|
||||
radial-gradient(1200px 680px at 50% 46%, rgba(17,17,17,0.18), rgba(17,17,17,0.74));
|
||||
}
|
||||
.has-bg-video .wrap { position: relative; z-index: 2; }
|
||||
/* Lift the copy off the moving backdrop. */
|
||||
.has-bg-video .eyebrow,
|
||||
.has-bg-video .h { text-shadow: 0 2px 22px rgba(0,0,0,0.7); }
|
||||
.has-bg-video .sub { color: #b9e6f4; text-shadow: 0 1px 14px rgba(0,0,0,0.75); }
|
||||
.hero.has-bg-video h1, .hero.has-bg-video .wordmark,
|
||||
.hero.has-bg-video .lede, .hero.has-bg-video .slogan { text-shadow: 0 2px 22px rgba(0,0,0,0.72); }
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.has-bg-video .sec-bg { animation: none; transform: scale(1.12); }
|
||||
}
|
||||
|
||||
/* Get started */
|
||||
.start {
|
||||
background: linear-gradient(180deg, var(--panel), var(--bg2));
|
||||
border: 1px solid var(--border); border-radius: 18px; padding: 40px; text-align: center;
|
||||
}
|
||||
.codeblock {
|
||||
display: inline-flex; align-items: center; gap: 14px; margin: 18px auto 8px;
|
||||
background: var(--bg2); border: 1px solid var(--border); border-radius: 10px;
|
||||
padding: 12px 16px; font-family: ui-monospace, monospace; font-size: 14px; color: var(--fg);
|
||||
}
|
||||
.codeblock .prompt { color: var(--accent); }
|
||||
.pill-row { display: flex; gap: 8px; justify-content: center; flex-wrap: wrap; margin-top: 44px; }
|
||||
.pill { font-size: 12.5px; color: var(--muted); border: 1px solid var(--border); border-radius: 999px; padding: 5px 12px; background: var(--panel); }
|
||||
|
||||
footer { border-top: 1px solid var(--border); padding: 30px 0; color: var(--muted); font-size: 13px; }
|
||||
footer .wrap { display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 12px; }
|
||||
|
||||
@media (max-width: 820px) {
|
||||
.grid { grid-template-columns: repeat(2, 1fr); }
|
||||
.shotrow { grid-template-columns: 1fr; }
|
||||
.nav-links a:not(.btn) { display: none; }
|
||||
}
|
||||
@media (max-width: 520px) {
|
||||
.grid { grid-template-columns: 1fr; }
|
||||
.tgrid .tcard { padding: 20px; gap: 16px; }
|
||||
.tcard .av { width: 64px; height: 64px; }
|
||||
.tcard .q { font-size: 15px; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<nav>
|
||||
<div class="wrap">
|
||||
<div class="brand">
|
||||
<svg class="boat" viewBox="0 0 32 32" width="24" height="24" aria-hidden="true"><path d="M16 4L16 22L6 22Z" fill="currentColor"/><path d="M16 8L16 22L24 22Z" fill="currentColor" opacity="0.6"/><path d="M4 24Q10 20 16 24Q22 28 28 24" stroke="currentColor" stroke-width="2.5" fill="none" stroke-linecap="round"/></svg>
|
||||
Odysseus
|
||||
</div>
|
||||
<div class="nav-links">
|
||||
<a href="#features">Features</a>
|
||||
<a href="#testimonials">Testimonials</a>
|
||||
<a href="#how">How it started</a>
|
||||
<a href="#start">Get started</a>
|
||||
<a class="btn" href="https://github.com/odysseus-ui/odysseus" target="_blank">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M12 .5C5.7.5.5 5.7.5 12c0 5.1 3.3 9.4 7.9 10.9.6.1.8-.2.8-.6v-2c-3.2.7-3.9-1.5-3.9-1.5-.5-1.3-1.3-1.7-1.3-1.7-1-.7.1-.7.1-.7 1.2.1 1.8 1.2 1.8 1.2 1 1.8 2.7 1.3 3.4 1 .1-.8.4-1.3.7-1.6-2.6-.3-5.3-1.3-5.3-5.7 0-1.3.5-2.3 1.2-3.1-.1-.3-.5-1.5.1-3.1 0 0 1-.3 3.3 1.2a11.5 11.5 0 0 1 6 0C17.3 4.7 18.3 5 18.3 5c.6 1.6.2 2.8.1 3.1.8.8 1.2 1.8 1.2 3.1 0 4.4-2.7 5.4-5.3 5.7.4.4.8 1.1.8 2.2v3.3c0 .4.2.7.8.6 4.6-1.5 7.9-5.8 7.9-10.9C23.5 5.7 18.3.5 12 .5z"/></svg>
|
||||
GitHub
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- HERO -->
|
||||
<header class="hero">
|
||||
<canvas id="hero-flow" aria-hidden="true"></canvas>
|
||||
<div class="wrap">
|
||||
<div class="hero-logo">
|
||||
<svg viewBox="0 0 32 32" width="48" height="48" aria-hidden="true"><path d="M16 4L16 22L6 22Z" fill="currentColor"/><path d="M16 8L16 22L24 22Z" fill="currentColor" opacity="0.6"/><path d="M4 24Q10 20 16 24Q22 28 28 24" stroke="currentColor" stroke-width="2.5" fill="none" stroke-linecap="round"/></svg>
|
||||
<span class="wordmark">Odysseus</span>
|
||||
</div>
|
||||
<p class="slogan">Yours for the voyage.</p>
|
||||
<h1>Your own <span class="grad">AI workspace</span>,<br>running on your hardware.</h1>
|
||||
<p class="lede">
|
||||
Odysseus is a self-hosted interface for talking to language models — chat,
|
||||
autonomous agents, tools, model serving, email, research, and more. Local-first,
|
||||
privacy-first, and no telemetry. Just you and your models.
|
||||
</p>
|
||||
<p style="font-size:11.5px; color:var(--muted); opacity:0.7; max-width:560px; margin:-18px auto 30px;">
|
||||
(if you want to add an API that's cool too — I'm not here to tell you how to live your life…)
|
||||
</p>
|
||||
<div class="hero-cta">
|
||||
<a class="btn primary" href="#start">Get started</a>
|
||||
<a class="btn" href="https://github.com/odysseus-ui/odysseus" target="_blank">View on GitHub</a>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- FEATURES -->
|
||||
<section id="features">
|
||||
<div class="wrap">
|
||||
<div class="center">
|
||||
<div class="eyebrow"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/><rect x="14" y="14" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/></svg>Everything, self-hosted</div>
|
||||
<h2 class="h">One app, a lot of capabilities</h2>
|
||||
<p class="sub">Started as an AI chat. Became a workspace. Each piece runs locally against
|
||||
whatever endpoints you point it at.</p>
|
||||
</div>
|
||||
<div class="grid">
|
||||
<div class="feature">
|
||||
<span class="ico"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg></span>
|
||||
<h3>Chat & Agents</h3>
|
||||
<p>Multi-turn chat plus autonomous agents that plan, call tools, and work through tasks.</p>
|
||||
</div>
|
||||
<div class="feature">
|
||||
<span class="ico"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.8-3.8a6 6 0 0 1-7.9 7.9l-6.9 6.9a2.1 2.1 0 0 1-3-3l6.9-6.9a6 6 0 0 1 7.9-7.9z"/></svg></span>
|
||||
<h3>Tools & MCP</h3>
|
||||
<p>Built-in tools (bash, files, web, memory) plus any MCP server you connect. Toggle per tool.</p>
|
||||
</div>
|
||||
<div class="feature">
|
||||
<span class="ico"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2 2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5"/></svg></span>
|
||||
<h3>Cookbook</h3>
|
||||
<p>Hardware-aware model recommendations and one-click serving across 270+ catalogued models.</p>
|
||||
</div>
|
||||
<div class="feature">
|
||||
<span class="ico"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="4" width="20" height="16" rx="2"/><path d="m22 7-10 5L2 7"/></svg></span>
|
||||
<h3>Email Assistant</h3>
|
||||
<p>AI summaries, style-matched draft replies, auto-tagging and spam triage over IMAP/SMTP.</p>
|
||||
</div>
|
||||
<div class="feature">
|
||||
<span class="ico"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="7"/><path d="M21 21l-4.3-4.3"/></svg></span>
|
||||
<h3>Deep Research</h3>
|
||||
<p>Multi-step research runs that gather, read, and synthesize sources into a written report.</p>
|
||||
</div>
|
||||
<div class="feature">
|
||||
<span class="ico"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="3" width="8" height="18" rx="1"/><rect x="14" y="3" width="8" height="18" rx="1"/></svg></span>
|
||||
<h3>Compare</h3>
|
||||
<p>Send one prompt to several models at once and compare their answers side-by-side.</p>
|
||||
</div>
|
||||
<div class="feature">
|
||||
<span class="ico"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><ellipse cx="12" cy="5" rx="9" ry="3"/><path d="M3 5v14c0 1.7 4 3 9 3s9-1.3 9-3V5"/><path d="M3 12c0 1.7 4 3 9 3s9-1.3 9-3"/></svg></span>
|
||||
<h3>Memory</h3>
|
||||
<p>Persistent memory the assistant builds up and recalls across all your conversations.</p>
|
||||
</div>
|
||||
<div class="feature">
|
||||
<span class="ico"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3l1.9 5.1L19 10l-5.1 1.9L12 17l-1.9-5.1L5 10l5.1-1.9z"/></svg></span>
|
||||
<h3>Skills <span style="font-size:10.5px;font-weight:700;color:var(--accent);border:1px solid var(--border);border-radius:999px;padding:1px 7px;margin-left:4px;vertical-align:middle;">self-evolving</span></h3>
|
||||
<p>The assistant writes, refines, and reuses its own skills — getting more capable over time.</p>
|
||||
</div>
|
||||
<div class="feature">
|
||||
<span class="ico"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg></span>
|
||||
<h3>Private by default</h3>
|
||||
<p>Runs on your machine against your own endpoints. No telemetry, with optional external integrations when you choose them.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- TESTIMONIALS (gag) -->
|
||||
<section id="testimonials" style="padding-top:30px;">
|
||||
<div class="wrap">
|
||||
<div class="center">
|
||||
<div class="eyebrow"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20.8 5.6a5.2 5.2 0 0 0-7.4 0L12 7l-1.4-1.4a5.2 5.2 0 1 0-7.4 7.4L12 21.4l8.8-8.4a5.2 5.2 0 0 0 0-7.4z"/></svg>Loved by enterprises</div>
|
||||
<h2 class="h">What our customers are saying</h2>
|
||||
</div>
|
||||
|
||||
<div class="tcarousel-wrap">
|
||||
<button class="tarrow prev" type="button" aria-label="Previous testimonial">‹</button>
|
||||
<div class="tgrid" id="tcarousel">
|
||||
|
||||
<!-- Coder guy -->
|
||||
<figure class="tcard">
|
||||
<span class="av"><img src="https://cdn.prod.website-files.com/66708f90d7e407423093fa76/66708f91d7e407423093fd21_john-carter-testimonial-image-dentistry-x-webflow-template.png" alt="Generic Coder Guy" loading="lazy"></span>
|
||||
<div class="tmeta">
|
||||
<p class="q">"Odysseus helped us ship more ships while shipping ships. Truly best-in-class shipping."</p>
|
||||
<div class="stars">★★★★★</div>
|
||||
<div class="nm">Generic Coder Guy</div>
|
||||
<div class="rl">Sr. Engineer, ShipShip Inc.</div>
|
||||
</div>
|
||||
</figure>
|
||||
|
||||
<!-- Woman -->
|
||||
<figure class="tcard">
|
||||
<span class="av"><img src="https://images.pexels.com/photos/5876695/pexels-photo-5876695.jpeg?auto=compress&cs=tinysrgb&w=160&h=160&fit=crop" alt="A real woman" loading="lazy"></span>
|
||||
<div class="tmeta">
|
||||
<p class="q">"I'm a real person. This is a real testimonial. By a real woman."</p>
|
||||
<div class="stars">★★★★★</div>
|
||||
<div class="nm">Generic Corporate Woman</div>
|
||||
<div class="rl">VP of Verticals, Things LLC</div>
|
||||
</div>
|
||||
</figure>
|
||||
|
||||
<!-- Cyclops -->
|
||||
<figure class="tcard cyclops" data-shake="1">
|
||||
<span class="av" style="border-color:rgba(255,90,90,0.6);">
|
||||
<svg viewBox="0 0 72 72" width="54" height="54" fill="none" stroke="#cbd5e1" stroke-width="2">
|
||||
<rect x="0" y="0" width="72" height="72" fill="#16241a"/>
|
||||
<circle cx="36" cy="32" r="18" fill="#7fae7f" stroke="#5a7a5a"/>
|
||||
<line x1="29" y1="22" x2="43" y2="34" stroke="#ff5a5a" stroke-width="3"/>
|
||||
<line x1="43" y1="22" x2="29" y2="34" stroke="#ff5a5a" stroke-width="3"/>
|
||||
<ellipse cx="36" cy="45" rx="7" ry="9" fill="#3a0a0a" stroke="#200"/>
|
||||
<path d="M31 51 l-1 4" stroke="#fff" stroke-width="2"/><path d="M41 51 l1 4" stroke="#fff" stroke-width="2"/>
|
||||
</svg>
|
||||
</span>
|
||||
<div class="tmeta">
|
||||
<p class="q">"AHHHHHHHHHHHHHHHHHHHHHHHHHHHHH"</p>
|
||||
<div class="stars zero">☆☆☆☆☆</div>
|
||||
<div class="nm">Polyphemus</div>
|
||||
<div class="rl">Cyclops, Cave Solutions (on leave)</div>
|
||||
</div>
|
||||
</figure>
|
||||
|
||||
<!-- Corporate -->
|
||||
<figure class="tcard">
|
||||
<span class="av">
|
||||
<svg viewBox="0 0 80 80" aria-hidden="true">
|
||||
<rect width="80" height="80" rx="18" fill="#111827"/>
|
||||
<circle cx="40" cy="29" r="14" fill="#d1d5db"/>
|
||||
<path d="M18 70c4-18 15-27 22-27s18 9 22 27" fill="#374151"/>
|
||||
<path d="M28 58h24l-5 12H33z" fill="#e06c75"/>
|
||||
<path d="M32 14h16l6 11H26z" fill="#f8fafc"/>
|
||||
</svg>
|
||||
</span>
|
||||
<div class="tmeta">
|
||||
<p class="q">"Anyway, as I was saying — best-in-class."</p>
|
||||
<div class="stars">★★★★★</div>
|
||||
<div class="nm">Chad Corporate</div>
|
||||
<div class="rl">Chief Executive Officer</div>
|
||||
</div>
|
||||
</figure>
|
||||
|
||||
</div>
|
||||
<button class="tarrow next" type="button" aria-label="Next testimonial">›</button>
|
||||
</div>
|
||||
<div class="tnav" id="tnav"></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
<!-- The one-shot prompt it started from (gag) -->
|
||||
<section style="padding-top:0;">
|
||||
<div class="wrap" style="text-align:center;">
|
||||
<p class="term-intro">Odysseus was created by a carefully crafted one-shot AI prompt:</p>
|
||||
<div class="term">
|
||||
<div class="term-bar">
|
||||
<span class="ttl">user@odysseus: ~</span>
|
||||
<span class="winbtns"><span data-term="min" title="Minimize">–</span><span class="x" data-term="close" title="Close">✕</span></span>
|
||||
</div>
|
||||
<pre id="term-pre"><span class="cs">></span> idk what to make come up with something oh make an AI chat but make it good and make it look nice</pre>
|
||||
</div>
|
||||
<button class="term-reopen" type="button">✕ reopen terminal</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- PREVIEWS — hover to expand + play -->
|
||||
<section id="previews">
|
||||
<div class="wrap">
|
||||
<div class="center">
|
||||
<div class="eyebrow"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2 12s3.6-7 10-7 10 7 10 7-3.6 7-10 7-10-7-10-7z"/><circle cx="12" cy="12" r="3"/></svg>See it in action</div>
|
||||
<h2 class="h">Hover to take a closer look</h2>
|
||||
<p class="sub center">Each panel expands and plays its preview when you hover it.</p>
|
||||
</div>
|
||||
<div class="previews">
|
||||
<div class="preview-panel" tabindex="0">
|
||||
<div class="ph"><svg width="30" height="30" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg><span>[ Chat & Agents ]</span></div>
|
||||
<video muted loop playsinline preload="none"><source src="chat.webm" type="video/webm"><source src="chat.mp4" type="video/mp4"></video>
|
||||
<div class="label"><span class="t"><svg class="ico" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>Chat & Agents</span><span class="desc">Talk to any local model, or give it tools and let the agent run.</span></div>
|
||||
</div>
|
||||
<div class="preview-panel" tabindex="0">
|
||||
<div class="ph"><svg width="30" height="30" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2 2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5"/></svg><span>[ Cookbook ]</span></div>
|
||||
<div class="label"><span class="t"><svg class="ico" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2 2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5"/></svg>Cookbook</span><span class="desc">Download, serve, and manage local models across your machines.</span></div>
|
||||
</div>
|
||||
<div class="preview-panel" tabindex="0">
|
||||
<div class="ph"><svg width="30" height="30" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="7"/><path d="m21 21-4.35-4.35"/></svg><span>[ Deep Research ]</span></div>
|
||||
<video muted loop playsinline preload="none"><source src="research.webm" type="video/webm"><source src="research.mp4" type="video/mp4"></video>
|
||||
<div class="label"><span class="t"><svg class="ico" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="7"/><path d="m21 21-4.35-4.35"/></svg>Deep Research</span><span class="desc">Ask once: it searches, reads sources, and writes back a cited report.</span></div>
|
||||
</div>
|
||||
<div class="preview-panel" tabindex="0">
|
||||
<div class="ph"><svg width="30" height="30" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="7" height="16" rx="1"/><rect x="14" y="4" width="7" height="16" rx="1"/></svg><span>[ Compare ]</span></div>
|
||||
<video muted loop playsinline preload="none"><source src="compare.webm" type="video/webm"><source src="compare.mp4" type="video/mp4"></video>
|
||||
<div class="label"><span class="t"><svg class="ico" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="7" height="16" rx="1"/><rect x="14" y="4" width="7" height="16" rx="1"/></svg>Compare</span><span class="desc">Send one prompt to many models at once and watch them answer side by side.</span></div>
|
||||
</div>
|
||||
<div class="preview-panel" tabindex="0">
|
||||
<div class="ph"><svg width="30" height="30" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><path d="M14 2v6h6"/><path d="M16 13H8M16 17H8M10 9H8"/></svg><span>[ Documents ]</span></div>
|
||||
<video muted loop playsinline preload="none"><source src="document.webm" type="video/webm"><source src="document.mp4" type="video/mp4"></video>
|
||||
<div class="label"><span class="t"><svg class="ico" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><path d="M14 2v6h6"/><path d="M16 13H8M16 17H8M10 9H8"/></svg>Documents</span><span class="desc">A document editor that puts you first — work on what you want, with AI help when you want it.</span></div>
|
||||
</div>
|
||||
<div class="preview-panel" tabindex="0">
|
||||
<div class="ph"><svg width="30" height="30" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="m3 7 2 2 4-4"/><path d="m3 17 2 2 4-4"/><path d="M13 6h8M13 18h8"/></svg><span>[ Notes & Tasks ]</span></div>
|
||||
<video muted loop playsinline preload="none"><source src="notes.webm" type="video/webm"><source src="notes.mp4" type="video/mp4"></video>
|
||||
<div class="label"><span class="t"><svg class="ico" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m3 7 2 2 4-4"/><path d="m3 17 2 2 4-4"/><path d="M13 6h8M13 18h8"/></svg>Notes & Tasks</span><span class="desc">Capture notes and to-dos, or let scheduled agents work and brief you after.</span></div>
|
||||
</div>
|
||||
<div class="preview-panel" tabindex="0">
|
||||
<div class="ph"><svg width="30" height="30" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="9" cy="9" r="2"/><path d="m21 15-3.6-3.6a2 2 0 0 0-2.8 0L6 21"/></svg><span>[ Image Gallery ]</span></div>
|
||||
<video muted loop playsinline preload="none"><source src="gallery.webm" type="video/webm"><source src="gallery.mp4" type="video/mp4"></video>
|
||||
<div class="label"><span class="t"><svg class="ico" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="9" cy="9" r="2"/><path d="m21 15-3.6-3.6a2 2 0 0 0-2.8 0L6 21"/></svg>Image Gallery</span><span class="desc">Generate, edit, remove backgrounds, and inpaint in your own gallery.</span></div>
|
||||
</div>
|
||||
<div class="preview-panel" tabindex="0">
|
||||
<div class="ph"><svg width="30" height="30" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2.7 6.3 8.4a8 8 0 1 0 11.4 0z"/></svg><span>[ Themes ]</span></div>
|
||||
<video muted loop playsinline preload="none"><source src="theme.webm" type="video/webm"><source src="theme.mp4" type="video/mp4"></video>
|
||||
<div class="label"><span class="t"><svg class="ico" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2.7 6.3 8.4a8 8 0 1 0 11.4 0z"/></svg>Themes</span><span class="desc">Restyle and make it yours — edit your own, or ask the agent to make one.</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- HOW IT STARTED -->
|
||||
<section id="how" class="has-bg-video">
|
||||
<video class="sec-bg" autoplay muted loop playsinline preload="auto"><source src="bg.webm" type="video/webm"><source src="bg.mp4" type="video/mp4"></video>
|
||||
<div class="sec-bg-tint"></div>
|
||||
<div class="wrap">
|
||||
<div class="eyebrow"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><path d="m15.6 8.4-2.1 5.1-5.1 2.1 2.1-5.1z"/></svg>How it actually started</div>
|
||||
<h2 class="h">Uncompromised local LLM experience.</h2>
|
||||
<p class="sub" style="max-width:760px;">
|
||||
I started working on the Odysseus project because running local AI felt fun and powerful.
|
||||
But the options at the time to engage with LLMs felt like taking steps back. The idea that you
|
||||
could just self-host AI and not pay for a subscription wasn't there. All the tools and functions
|
||||
that make it all magic were missing.
|
||||
</p>
|
||||
<p class="sub" style="max-width:760px; margin-top:14px;">
|
||||
So I started building Odysseus bit by bit — and the more I gave it to work with, the
|
||||
better it served me. Turns out the more your model knows about you, the more useful it gets.
|
||||
Which is the other reason to self-host: you get all that context without handing your private
|
||||
data to someone else's cloud. </p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- GET STARTED -->
|
||||
<section id="start">
|
||||
<div class="wrap">
|
||||
<div class="start">
|
||||
<div class="eyebrow"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 17l6-5-6-5"/><path d="M12 19h8"/></svg>Get started</div>
|
||||
<h2 class="h" style="margin-bottom:6px;">Odysseus is yours.</h2>
|
||||
<p class="sub center" style="margin:0 auto;">It's open source and free. No sales team, no demo request, no Trojan horse.</p>
|
||||
<div class="codeblock"><span class="prompt">$</span> git clone https://github.com/odysseus-ui/odysseus.git && cd odysseus</div>
|
||||
<div>
|
||||
<a class="btn primary" href="https://github.com/odysseus-ui/odysseus" target="_blank" style="margin-top:14px;">View on GitHub</a>
|
||||
</div>
|
||||
<div class="pill-row">
|
||||
<span class="pill">Self-hosted</span>
|
||||
<span class="pill">Bring your own models</span>
|
||||
<span class="pill">Local-first</span>
|
||||
<span class="pill">MCP-ready</span>
|
||||
<span class="pill">No telemetry</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<footer>
|
||||
<div class="wrap">
|
||||
<div>© 2026 Odysseus · Built from one prompt that refused to stop.</div>
|
||||
<div>No cyclopes were harmed in production.<sup>*</sup></div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script>
|
||||
// Hero background: Perlin flow field — colored particle streams (ported from
|
||||
// the app's own background effect, scoped to the hero and brand-colored).
|
||||
(function () {
|
||||
var canvas = document.getElementById('hero-flow');
|
||||
if (!canvas) return;
|
||||
if (window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;
|
||||
var hero = canvas.parentElement, ctx = canvas.getContext('2d');
|
||||
var dpr = Math.min(window.devicePixelRatio || 1, 2);
|
||||
var W, H, t = 0, particles = [];
|
||||
var COLORS = ['#9cdef2', '#e06c75', '#5fb6cc']; // cyan, coral, teal
|
||||
var FADE = 'rgba(40,44,52,0.06)'; // trail fade toward --bg
|
||||
function n2(x, y) { var n = Math.sin(x * 12.9898 + y * 78.233) * 43758.5453; return n - Math.floor(n); }
|
||||
function noise(x, y) {
|
||||
var ix = Math.floor(x), iy = Math.floor(y), fx = x - ix, fy = y - iy;
|
||||
var a = n2(ix, iy), b = n2(ix + 1, iy), c = n2(ix, iy + 1), d = n2(ix + 1, iy + 1);
|
||||
var ux = fx * fx * (3 - 2 * fx), uy = fy * fy * (3 - 2 * fy);
|
||||
return a + (b - a) * ux + (c - a) * uy + (a - b - c + d) * ux * uy;
|
||||
}
|
||||
function resize() {
|
||||
W = hero.clientWidth; H = hero.clientHeight;
|
||||
canvas.width = W * dpr; canvas.height = H * dpr;
|
||||
canvas.style.width = W + 'px'; canvas.style.height = H + 'px';
|
||||
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
||||
if (!particles.length) {
|
||||
for (var i = 0; i < 260; i++) particles.push({ x: Math.random() * W, y: Math.random() * H, life: Math.random(), c: COLORS[i % COLORS.length] });
|
||||
}
|
||||
}
|
||||
resize();
|
||||
window.addEventListener('resize', resize);
|
||||
function draw() {
|
||||
requestAnimationFrame(draw);
|
||||
ctx.fillStyle = FADE; ctx.fillRect(0, 0, W, H);
|
||||
for (var i = 0; i < particles.length; i++) {
|
||||
var p = particles[i];
|
||||
var ang = noise(p.x * 0.004 + t * 0.0008, p.y * 0.004 + 100) * Math.PI * 6;
|
||||
var sp = 1 + noise(p.x * 0.003, p.y * 0.003 + 50) * 1.5;
|
||||
p.x += Math.cos(ang) * sp; p.y += Math.sin(ang) * sp; p.life -= 0.001;
|
||||
if (p.life <= 0 || p.x < 0 || p.x > W || p.y < 0 || p.y > H) { p.x = Math.random() * W; p.y = Math.random() * H; p.life = 1; }
|
||||
ctx.beginPath(); ctx.arc(p.x, p.y, 1.1, 0, Math.PI * 2);
|
||||
ctx.fillStyle = p.c; ctx.globalAlpha = p.life * 0.18; ctx.fill();
|
||||
}
|
||||
ctx.globalAlpha = 1; t++;
|
||||
}
|
||||
draw();
|
||||
})();
|
||||
|
||||
// Typewriter for the origin terminal: type line 1, pause 2s, line 2, pause
|
||||
// 2s, line 3, hold 4s, then reset and loop. Blinking "|" cursor throughout.
|
||||
(function () {
|
||||
var pre = document.getElementById('term-pre');
|
||||
if (!pre) return;
|
||||
var lines = [
|
||||
{ p: '<span class="cs">></span> ', t: 'idk what to make come up with something oh make an AI chat but make it good and make it look nice' }
|
||||
];
|
||||
var CURSOR = '<span class="term-cursor">|</span>';
|
||||
var TYPE_MS = 40;
|
||||
var done = [], li = 0, timer = null;
|
||||
|
||||
function render(partial) {
|
||||
pre.innerHTML = done.join('\n') + (done.length ? '\n' : '') + partial + CURSOR;
|
||||
}
|
||||
function typeLine() {
|
||||
var ln = lines[li], i = 0;
|
||||
(function step() {
|
||||
if (i <= ln.t.length) {
|
||||
render(ln.p + ln.t.slice(0, i));
|
||||
i++; timer = setTimeout(step, TYPE_MS);
|
||||
} else {
|
||||
done.push(ln.p + ln.t);
|
||||
li++;
|
||||
if (li >= lines.length) timer = setTimeout(reset, 4000); // hold last line 4s
|
||||
else timer = setTimeout(typeLine, 2000); // pause 2s before next
|
||||
}
|
||||
})();
|
||||
}
|
||||
function reset() { clearTimeout(timer); done = []; li = 0; typeLine(); }
|
||||
|
||||
// Start typing only when the terminal scrolls into view (and replay each
|
||||
// time you return to it).
|
||||
if ('IntersectionObserver' in window) {
|
||||
var io2 = new IntersectionObserver(function (entries) {
|
||||
entries.forEach(function (e) { if (e.isIntersecting) reset(); });
|
||||
}, { threshold: 0.45 });
|
||||
io2.observe(pre);
|
||||
} else {
|
||||
reset();
|
||||
}
|
||||
})();
|
||||
|
||||
// Previews: hovering a panel expands it (CSS) and plays its video; the
|
||||
// video only becomes visible once it actually starts playing, so missing
|
||||
// files just leave the labeled placeholder.
|
||||
(function () {
|
||||
document.querySelectorAll('.preview-panel').forEach(function (p) {
|
||||
var v = p.querySelector('video');
|
||||
if (!v) return;
|
||||
v.addEventListener('playing', function () { p.classList.add('has-video'); });
|
||||
v.addEventListener('pause', function () { /* keep last frame */ });
|
||||
var play = function () { var pr = v.play(); if (pr && pr.catch) pr.catch(function () {}); };
|
||||
p.addEventListener('mouseenter', play);
|
||||
p.addEventListener('focus', play);
|
||||
p.addEventListener('mouseleave', function () { v.pause(); });
|
||||
p.addEventListener('blur', function () { v.pause(); });
|
||||
p.addEventListener('click', function () { if (v.paused) play(); else v.pause(); });
|
||||
});
|
||||
})();
|
||||
|
||||
// Domino reveal: fade/slide each section in as it scrolls into view.
|
||||
(function () {
|
||||
var els = document.querySelectorAll('.hero, section');
|
||||
if (!('IntersectionObserver' in window)) {
|
||||
els.forEach(function (e) { e.classList.add('in'); });
|
||||
return;
|
||||
}
|
||||
var io = new IntersectionObserver(function (entries) {
|
||||
entries.forEach(function (e) {
|
||||
if (e.isIntersecting) { e.target.classList.add('in'); io.unobserve(e.target); }
|
||||
});
|
||||
}, { threshold: 0.12, rootMargin: '0px 0px -8% 0px' });
|
||||
els.forEach(function (e) { io.observe(e); });
|
||||
})();
|
||||
|
||||
// Fake terminal window buttons — minimize, maximize, close (and reopen).
|
||||
(function () {
|
||||
var term = document.querySelector('.term');
|
||||
var reopen = document.querySelector('.term-reopen');
|
||||
if (!term) return;
|
||||
term.querySelectorAll('.winbtns [data-term]').forEach(function (b) {
|
||||
b.addEventListener('click', function () {
|
||||
var act = b.getAttribute('data-term');
|
||||
if (act === 'min') term.classList.toggle('term-min');
|
||||
else if (act === 'close') {
|
||||
term.classList.add('term-closed');
|
||||
if (reopen) reopen.classList.add('show');
|
||||
}
|
||||
});
|
||||
});
|
||||
if (reopen) reopen.addEventListener('click', function () {
|
||||
term.classList.remove('term-closed', 'term-min');
|
||||
reopen.classList.remove('show');
|
||||
});
|
||||
})();
|
||||
|
||||
// Mobile testimonial carousel: tap or swipe to advance; Polyphemus shakes ~1s.
|
||||
(function () {
|
||||
var carousel = document.getElementById('tcarousel');
|
||||
var nav = document.getElementById('tnav');
|
||||
if (!carousel || !nav) return;
|
||||
var cards = [].slice.call(carousel.querySelectorAll('.tcard'));
|
||||
if (!cards.length) return;
|
||||
var idx = 0;
|
||||
|
||||
var dots = cards.map(function (_, k) {
|
||||
var d = document.createElement('span');
|
||||
d.className = 'tdot';
|
||||
d.addEventListener('click', function (e) { e.stopPropagation(); show(k); });
|
||||
nav.appendChild(d);
|
||||
return d;
|
||||
});
|
||||
var hint = document.createElement('div');
|
||||
hint.className = 'thint';
|
||||
hint.textContent = 'tap or swipe for the next satisfied customer →';
|
||||
nav.appendChild(hint);
|
||||
|
||||
function show(i) {
|
||||
idx = (i + cards.length) % cards.length;
|
||||
cards.forEach(function (c, k) { c.classList.toggle('active', k === idx); c.classList.remove('shake'); });
|
||||
dots.forEach(function (d, k) { d.classList.toggle('on', k === idx); });
|
||||
var cur = cards[idx];
|
||||
if (cur.getAttribute('data-shake') === '1') {
|
||||
void cur.offsetWidth;
|
||||
cur.classList.add('shake');
|
||||
setTimeout(function () { cur.classList.remove('shake'); }, 1000);
|
||||
}
|
||||
}
|
||||
|
||||
carousel.addEventListener('click', function () { show(idx + 1); });
|
||||
|
||||
var _prev = document.querySelector('.tarrow.prev');
|
||||
var _next = document.querySelector('.tarrow.next');
|
||||
if (_prev) _prev.addEventListener('click', function (e) { e.stopPropagation(); show(idx - 1); });
|
||||
if (_next) _next.addEventListener('click', function (e) { e.stopPropagation(); show(idx + 1); });
|
||||
|
||||
var sx = null;
|
||||
carousel.addEventListener('touchstart', function (e) { sx = e.touches[0].clientX; }, { passive: true });
|
||||
carousel.addEventListener('touchend', function (e) {
|
||||
if (sx === null) return;
|
||||
var dx = e.changedTouches[0].clientX - sx;
|
||||
if (Math.abs(dx) > 30) { show(idx + (dx < 0 ? 1 : -1)); }
|
||||
sx = null;
|
||||
});
|
||||
|
||||
show(0);
|
||||
})();
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 1003 KiB |
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 45 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 2.5 MiB |
Binary file not shown.
@@ -0,0 +1,26 @@
|
||||
# Skills lifecycle
|
||||
|
||||
The UI exposes All, Built-in, Approved, and Draft. Draft includes archived
|
||||
records so they remain inspectable and recoverable. Built-ins are not audited.
|
||||
Approved means published, passing, at the configured confidence threshold,
|
||||
and not marked unnecessary. Baseline speed measurements remain evidence, not
|
||||
an additional hidden UI approval gate.
|
||||
|
||||
Automatic audits process at most eight eligible records at a time, oldest first.
|
||||
New records are eligible immediately; inconclusive checks retry after a day;
|
||||
failed repairs retry after a week. Passed, duplicate-skipped, and archived records
|
||||
are excluded. Existing daily Skills Audit tasks drive this queue. Their quiet
|
||||
window deferrals propagate to the scheduler rather than becoming task failures.
|
||||
Automatic runs use background model scheduling. Existing self-repair and teacher
|
||||
repair stages remain in place; failed candidates remain drafts.
|
||||
|
||||
The skill index advertises short descriptions; the agent loads a relevant full
|
||||
procedure on demand and applies already-injected procedures directly. Extraction
|
||||
prefers verified discoveries and specific workarounds over routine tool usage.
|
||||
|
||||
Reference reviewed: NousResearch/hermes-agent, MIT license, commit
|
||||
cfdbbb6e35010ace89fbe8243ee82fa4de143e10, cloned to
|
||||
/home/pewds/hermes-skills-reference. In particular tools/skills_tool.py and
|
||||
agent/prompt_builder.py use progressive disclosure and task-triggered procedure
|
||||
loading. These changes adapt that approach to Odysseus's existing registry;
|
||||
no Hermes implementation code was copied.
|
||||
Binary file not shown.
Reference in New Issue
Block a user