docs: publish architecture and experiment portal

This commit is contained in:
wuyang
2026-07-26 21:03:23 +08:00
parent 4eb915283d
commit 688ddec1fe
17 changed files with 3582 additions and 56 deletions
-1
View File
@@ -21,4 +21,3 @@ dist
*.sqlite3
*.db
tests
docs
+7
View File
@@ -5,6 +5,8 @@ same independently evolvable Agent loop; its scheduler, context policy, memory,
tools, evidence rules, and child Agents are owned by this repository.
The production instance is available at <https://agent.k1412.top>.
Architecture, Agent internals, and the experiment board are published at
<https://agent.k1412.top/doc/>.
## Architecture
@@ -120,9 +122,14 @@ Open WebUI and re-signed for the Workspace Gateway; no internal key is exposed.
## Documentation
- [Documentation index](docs/README.md)
- [Architecture and Agent loop](docs/architecture.md)
- [Agent loop implementation](docs/agent-loop.md)
- [Experiment system](docs/experiments.md)
- [Project history and decisions](docs/project-history.md)
- [Open WebUI integration](docs/openwebui-integration.md)
- [Infrastructure map](docs/infrastructure.md)
- [Development and verification](docs/development.md)
- [Operations runbook](docs/operations.md)
- [Security model](docs/security.md)
+1
View File
@@ -80,6 +80,7 @@ WORKDIR /app/backend
COPY --from=web-build /src/backend /app/backend
COPY --from=web-build /src/build /app/build
COPY --from=web-build /src/CHANGELOG.md /app/CHANGELOG.md
COPY docs/site /app/build/doc
COPY --from=web-build /src/LICENSE /app/OPEN_WEBUI_LICENSE
COPY THIRD_PARTY_NOTICES.md /app/THIRD_PARTY_NOTICES.md
+55
View File
@@ -0,0 +1,55 @@
# K1412 Agent documentation
This directory is the canonical technical record for K1412 Agent. The public
documentation portal at <https://agent.k1412.top/doc/> is a curated rendering
of the same design. When behavior, infrastructure, or an experiment changes,
update the relevant Markdown document and the portal snapshot in the same
commit.
## Start here
| Document | Audience | What it answers |
| --- | --- | --- |
| [Architecture](architecture.md) | Everyone | What runs where, and why are the boundaries drawn this way? |
| [Agent loop](agent-loop.md) | Agent researchers and backend engineers | How do context, tools, scheduling, evidence, memory, and child Agents work? |
| [Experiment system](experiments.md) | Agent researchers | How do we change the loop without losing comparability or production safety? |
| [Project history](project-history.md) | Maintainers | Which product and architecture decisions have already been made? |
| [Open WebUI integration](openwebui-integration.md) | Frontend and platform engineers | What belongs to Open WebUI, and what remains K1412-owned? |
| [Security model](security.md) | Security and platform engineers | How are users, credentials, workspaces, and Docker separated? |
| [Infrastructure map](infrastructure.md) | Operators | Which services are shared infrastructure and which are Agent core? |
| [Development and verification](development.md) | Contributors | How do I run, test, and safely change the system? |
| [Operations runbook](operations.md) | Operators | How is production built, deployed, verified, backed up, and migrated? |
## Current product contract
- There is one user-facing mode: the K1412 Agent loop. The earlier Chat/Work
split was removed deliberately.
- Open WebUI owns accounts, sessions, approval, conversation storage, and the
chat shell. K1412 owns Agent behavior.
- Four server-defined model choices are exposed: Luna, Terra, Sol, and
DeepSeek V4 Pro. Model identity and thinking capability are displayed
separately.
- Every Open WebUI user receives an isolated Docker container, network, and
persistent volume on the dedicated physical execution host.
- Generated files can be browsed and downloaded through the authenticated Web
UI. Internal dependency directories are hidden.
- Provider credentials, internal prompts, Docker access, SSH configuration,
and tool-service credentials are never browser-configurable.
## What is experimental
The stable platform boundary is authentication, user/workspace isolation,
durable storage, and the public OpenAI-compatible Runtime API. The intended
experimental surface is:
- context selection and compaction;
- memory retrieval and lifecycle;
- tool descriptions, normalization, and routing;
- parallel scheduling and mutation serialization;
- child-Agent delegation policy;
- evidence and completion gates;
- recovery prompts and retry policy;
- model routing and per-tier budgets.
Every experiment should be versioned in run events and evaluated against a
fixed task set before production rollout. See [experiments.md](experiments.md).
+223
View File
@@ -0,0 +1,223 @@
# Agent loop implementation
## Purpose
The K1412 loop turns a model completion API into an accountable coding Agent.
Its primary rule is simple: a textual claim is not proof that work happened.
Files, commands, reports, and tests must be backed by successful tool events
from the current run.
The current event metadata identifies the implementation as:
- strategy: `agent-loop-v3`;
- scheduler: `safe-parallel-v1`;
- context policy: `recent-visible-v1`.
These identifiers are part of the experiment contract, not marketing version
numbers. Change them when behavior changes in a way that can affect evaluation.
## Run state machine
```mermaid
stateDiagram-v2
[*] --> BuildContext
BuildContext --> RequestModel
RequestModel --> ValidateCalls: tool calls
RequestModel --> CheckCompletion: no tool calls
ValidateCalls --> Schedule
Schedule --> Execute
Execute --> RecordEvidence
RecordEvidence --> RequestModel
CheckCompletion --> RequestModel: evidence missing and budget remains
CheckCompletion --> Completed: evidence sufficient
CheckCompletion --> Unverified: evidence missing and retry budget exhausted
Completed --> [*]
Unverified --> [*]
```
Every top-level run receives a unique `run_id`. All events include the user,
chat, monotonic sequence, timestamp, event type, and a sanitized payload.
## Context policy
`ContextPolicy.prepare`:
1. accepts only system, developer, user, and assistant messages;
2. strips rendered `<details type="tool_calls">` blocks from old assistant
messages so UI markup is not fed back as model context;
3. prepends the K1412 system contract;
4. appends up to eight durable user memories as context, explicitly not as
higher-priority instructions;
5. walks visible messages newest-first until the model's character budget is
reached;
6. records how many old messages were dropped.
The policy is intentionally simple and inspectable. It does not yet summarize
old conversation branches, retrieve semantic workspace context, or estimate
provider-specific tokenization. Those are future experiment candidates.
## Model contract
`ModelSpec` is the server-side source of truth for:
- public and provider model IDs;
- provider selection;
- thinking capability and display label;
- reasoning effort when the provider supports it;
- maximum output tokens;
- maximum loop iterations;
- context character budget.
Luna, Terra, and Sol are separate local models with boolean thinking support;
they are not three effort settings for one model. DeepSeek V4 Pro uses the
DeepSeek provider with thinking enabled and `reasoning_effort=max`.
Runtime preserves provider `reasoning_content` across tool turns when required
for protocol correctness, but does not publish hidden reasoning as user-facing
content.
## Tool catalog
Workspace tools:
- status, list, read, search, write, and patch files;
- execute foreground commands;
- inspect Git status and diff;
- start, poll, and cancel background processes.
Runtime state tools:
- update a per-chat plan;
- remember, recall, and forget durable user memory.
Delegation tool:
- start one bounded child Agent with a role, task, and explicit write policy.
Tool schemas use `additionalProperties: false` so malformed model arguments
fail early. Arguments are normalized before policy checks. Content and patch
bodies are omitted from public run-event payloads.
## Intent-based tool selection
The root loop does not always send every tool. Lightweight request classifiers
detect whether the task concerns artifacts, execution, source files, reports,
comparisons, processes, Git, memory, or delegation. The selected catalog keeps
the model's tool decision smaller while retaining the core workspace tools.
This is heuristic routing, not authorization. Gateway remains the enforcement
boundary.
## Scheduler
Each model response can request at most eight tool calls. Calls are parsed in
their original order and divided into consecutive groups:
- tools marked `parallel_safe` execute concurrently;
- mutations and other non-parallel tools execute serially;
- read-only child Agents may run in parallel;
- a child with write access is serialized;
- child Agents cannot delegate again.
Gateway also serializes workspace mutations per user. This second lock is
important because multiple Runtime requests or browser actions can address the
same workspace.
The scheduler preserves result order even when a parallel group completes out
of order.
## Defensive normalization and retry guards
The loop repairs a small set of common model formatting mistakes, then applies
policy:
- reject bare interactive commands such as `python3`, `bash`, or `node`;
- reject attempts to run documentation/data files as executable source;
- reject syntactically invalid complete Python files before writing;
- decode repeated escaped source-layout newlines produced by weak models;
- skip identical successful writes;
- block an identical failed write until content changes;
- block an unchanged failed command until a mutation or materially different
diagnostic occurs;
- deduplicate repeated commands in one batch;
- cap generation, iteration, and tool-batch sizes.
At the execution layer, Bash runs with `pipefail` and returns the true exit
code. Foreground tools default to a 900-second ceiling; background processes
have explicit start/poll/cancel lifecycle.
## Evidence-gated completion
The loop derives required evidence from the user request.
| Request shape | Minimum evidence |
| --- | --- |
| Concrete artifact | A successful file mutation and a later successful verification |
| Execute/test/analyze | At least one successful execution or inspection |
| Source-code request | A separate executable source file |
| Report request | A separate report written after execution |
| Benchmark/comparison report | Exact numeric measurements copied from successful execution output |
Verification includes a later file read, Git diff/status, command execution, or
background-process poll. A failed command remains unresolved until a later
execution succeeds.
If a model tries to finish too early, Runtime emits `completion.rejected`,
returns a focused recovery instruction to the model, and continues. After
repeated rejected checkpoints or iteration exhaustion, Runtime returns an
explicit incomplete result instead of laundering an unverified claim into a
success.
## Delegation
A root Agent can delegate a bounded independent task. The child receives:
- a role and exact task;
- a reduced iteration budget of at most eight;
- no delegation tool;
- read-only tools unless write access was explicitly requested.
Child events share the root run stream with an increased `depth`. The current
implementation is recursive execution inside one Runtime process, not a
distributed queue or a persistent autonomous worker.
## Memory and plans
Durable memories are scoped by user ID. Recall is currently recency-first with
optional case-insensitive substring filtering. Plans are scoped by user and
chat and allow at most one `in_progress` item.
Memory is useful but intentionally conservative: the system does not
automatically extract facts from every conversation and does not yet perform
embedding retrieval, confidence scoring, expiry, or conflict resolution.
## Run event model
Important event types include:
- `run.created`, `context.built`, `run.completed`, `run.failed`,
`run.cancelled`;
- `model.requested`, `model.responded`;
- `tool.started`, `tool.completed`, `tool.batch_limited`;
- `completion.rejected`, `completion.unverified`;
- `agent.spawned`, `agent.completed`.
The event stream powers UI detail blocks today and is the basis for future
replay, evaluation, cost analysis, and A/B assignment. Events store public
arguments and concise summaries, not credentials, full write bodies, or hidden
reasoning.
## Known limitations
- Context compaction drops old messages instead of summarizing them.
- Memory retrieval is lexical rather than semantic.
- Tool intent classifiers are regular-expression based.
- The scheduler parallelizes only consecutive safe calls and has no resource
cost model.
- Child Agents are not durable across Runtime restarts.
- Experiment assignment and aggregate dashboards are not yet implemented.
- Provider token accounting is retained when returned but is not yet converted
into a unified cost model.
These are deliberate experiment targets, documented in
[experiments.md](experiments.md).
+164 -53
View File
@@ -1,74 +1,185 @@
# Architecture
## Single Agent path
## Design objective
Open WebUI owns authenticated users, conversation history, and rendering.
Every normal model request is delegated to the K1412 Runtime. There is no
second Chat loop and no per-conversation mode state.
K1412 Agent is a multi-user web coding Agent whose loop can be changed
independently of its account system and user interface. The architecture keeps
the high-cost experimental surface small: researchers should be able to change
context, memory, scheduling, tools, delegation, or completion policy without
forking authentication, chat history, Docker lifecycle, or the whole frontend.
## Agent loop
The system deliberately has one Agent path. The earlier idea of a lightweight
Chat loop beside a custom Work loop was removed because it duplicated product
behavior, confused model selection, and increased the amount of frontend and
backend code that had to remain compatible.
The first strategy is intentionally modular:
## System context
- `ModelProvider` owns provider transport.
- `ContextPolicy` owns visible-message cleanup and compaction.
- `ToolRegistry` owns tool schemas and dispatch.
- `AgentLoop` owns iteration, safe parallel scheduling, and delegation.
- `RuntimeStore` owns events, plans, and durable Agent memory.
- `ExecutionProvider` owns local or SSH Docker execution.
```mermaid
flowchart LR
U["Browser user"] --> P["DNS, TLS and Nginx Proxy Manager"]
P --> W["Open WebUI<br/>auth, RBAC, history, UI"]
W --> R["Agent Runtime<br/>loop, context, memory, model routing"]
R --> M["Model providers<br/>K1412 API or DeepSeek"]
R --> G["Workspace Gateway<br/>identity and execution policy"]
G -->|Tailscale + SSH Docker| X["Per-user workspace container<br/>home-node-itx"]
W --> DB["PostgreSQL<br/>users and chats"]
R --> DB
W --> Q["Redis<br/>coordination"]
```
Every run records its model ID, strategy version, scheduler version, context
policy, model/tool events, failures, and completion. This event stream is the
foundation for replay, evaluation, and future experiment assignment.
Only Web is publicly reachable. Runtime, Gateway, PostgreSQL, and Redis live on
private Compose networks. Workspace containers run on a separate physical
Docker host, receive no application secrets, and expose no host ports.
Luna, Terra, and Sol are three independent Ollama models. Each advertises
boolean thinking support, but none advertises adjustable reasoning effort.
DeepSeek V4 Pro is a fourth model configured with thinking enabled and maximum
reasoning effort. Provider selection and credentials remain server-side.
Thinking state is preserved across tool turns when the provider requires the
assistant's `reasoning_content` to be returned with following tool results.
The Agent never publishes hidden reasoning as its own user-facing output.
## Responsibility boundaries
Completion is evidence-gated. Requests for scripts, reports, code, or
other concrete artifacts are not accepted as complete until a file mutation
has succeeded and a later read, diff, or command has verified the latest
mutation. Bare interactive shell commands are rejected. If the iteration
budget is exhausted without evidence, Runtime returns an explicit incomplete
checkpoint instead of repeating an unverified model claim.
| Layer | Owns | Must not own |
| --- | --- | --- |
| Open WebUI | Registration, login, sessions, admin approval, RBAC, chat persistence, rendering | Agent scheduling, provider secrets, Docker access |
| Agent Runtime | Model catalog, context policy, Agent loop, tools, scheduling, delegation, memory, plans, run events, evidence gates | Browser sessions, Docker socket |
| Workspace Gateway | Signed identity enforcement, workspace path policy, per-user locks, Docker lifecycle, file delivery | Model calls, public authentication UI |
| Workspace container | User code, dependencies, generated files, command execution | Other users' data, service credentials, Docker socket |
| PostgreSQL | Open WebUI durable state plus Runtime events, plans, and memories | Execution |
| Shared infrastructure | TLS, registry, source hosting, private routing, model serving | Agent behavior |
Read-only tools and read-only child Agents may run concurrently. Workspace
mutations are serialized. A child Agent cannot delegate again, and read-only
children receive only read-only tools.
This separation lets the Web UI be upgraded on its own schedule while the
Agent loop can iterate frequently.
## Execution providers
## Request lifecycle
`local-docker` connects the Workspace Gateway to the local Docker daemon.
`ssh-docker` connects the same Docker SDK operations to a remote physical
machine through SSH. Both create the same workspace image, persistent per-user
volume, and dedicated per-user bridge network, so moving execution off the web
host changes configuration rather than Agent behavior.
### Agent request
For SSH mode, layer `compose.ssh.yaml` over the development Compose file, or
`deploy/docker-compose.ssh.yml` over the two production Compose files. The
override removes the local Docker socket and mounts only the selected SSH
configuration directory read-only.
```mermaid
sequenceDiagram
participant B as Browser
participant W as Open WebUI
participant R as Runtime
participant M as Model provider
participant G as Gateway
participant X as User workspace
Only Open WebUI publishes a host port. Runtime, Gateway, Postgres, and Redis
remain unpublishing services. Runtime and Gateway also join an egress-only
bridge so the model API and a future SSH Docker host are reachable without
exposing either service on the host.
B->>W: Authenticated chat request
W->>R: OpenAI-compatible request + service key + signed user identity
R->>R: Verify identity, build context, record run.created
loop Until evidence-backed completion or budget exhausted
R->>M: Recent context + tools + provider policy
M-->>R: Assistant content and/or tool calls
R->>R: Normalize, validate, limit, and schedule calls
R->>G: Tool request + service key + freshly signed identity
G->>G: Verify identity and bind user workspace
G->>X: Execute with resource and path constraints
X-->>G: Exit code, output, or file result
G-->>R: Structured tool result
R->>R: Persist events and evaluate completion evidence
end
R-->>W: SSE answer and completed tool detail blocks
W-->>B: Rendered Agent response
```
## Workspace file path
The identity sent by Open WebUI is verified once when Runtime accepts the
request. Runtime then signs the already verified identity again for every
Gateway call. This prevents a long Agent run from reusing an identity token
that expired while the model or a tool was working.
### Workspace file request
```text
browser with Open WebUI session
-> /api/v1/k1412/workspace/*
-> Open WebUI backend verifies the user
-> backend mints a short-lived signed identity JWT
-> Workspace Gateway verifies service key + user JWT
-> provider selects only that user's hashed container and volume
-> Open WebUI verifies the logged-in user
-> Web signs a short-lived user identity
-> Gateway verifies service key + signed identity
-> Gateway selects only the hashed container and volume for that user
-> browse, download, or streamed archive response
```
The file API exposes read-only browse, file download, and streamed directory
archive operations. Paths pass through the same `/workspace` normalization as
Agent tools. The browser never talks to Gateway or Docker directly.
The browser never receives a Gateway service key and never connects to Docker.
## Runtime internals
Runtime is an OpenAI-compatible provider from Open WebUI's point of view, but
normal chat completions are implemented by the K1412 loop:
- `ModelProvider` translates the internal model spec to a provider request and
normalizes streamed completion responses.
- `ContextPolicy` removes rendered tool-detail markup, prepends the system
contract and durable memories, and keeps the newest visible messages within
a per-model character budget.
- `ToolRegistry` declares tool schemas and routes workspace tools to Gateway or
state tools to `RuntimeStore`.
- `AgentLoop` owns iteration, tool normalization, validation, scheduling,
recovery, delegation, and evidence-gated completion.
- `RuntimeStore` persists run events, per-chat plans, and user memories.
More detail is in [agent-loop.md](agent-loop.md).
## Workspace architecture
`local-docker` and `ssh-docker` implement the same `ExecutionProvider`
contract. Production uses `ssh-docker`: Gateway talks to Docker on
`home-node-itx` through a dedicated SSH configuration mounted read-only.
Each immutable Open WebUI user ID maps to a SHA-256-derived workspace ID and
therefore to exactly one:
- container named `k1412-ws-<workspace-id>`;
- named volume `k1412-ws-data-<workspace-id>`;
- bridge network `k1412-ws-net-<workspace-id>`.
Containers run as UID/GID 1000 with a read-only root filesystem, all Linux
capabilities dropped, `no-new-privileges`, PID/CPU/memory limits, no Docker
socket, and no published ports. `/workspace` is the only persistent writable
filesystem. `/tmp` is writable but mounted `noexec`.
Python environments live at `/workspace/.venv`. User-level home and package
caches live below `/workspace/.agent`. The image puts the virtual environment
and user binary directories first on `PATH`. Commands run under Bash with
`pipefail`, so a failed install cannot be hidden by a successful `tail` or
similar pipeline stage.
When `WORKSPACE_IMAGE` changes to a new immutable tag, Gateway replaces a stale
container on its next access but reattaches the existing named volume. Image
upgrades therefore change the execution environment without deleting user
files.
## Networks and deployment
Production runs as one Unraid Compose Manager project:
- `public`: Web ingress;
- `internal`: private service-to-service traffic;
- `egress`: Runtime provider access and Gateway SSH access.
Only Web publishes NAS port `12004`. Nginx Proxy Manager on the VPS terminates
TLS for `agent.k1412.top` and forwards over Tailscale. Images are built for
`linux/amd64`, pushed to `docker.k1412.top/wuyang/*` with immutable
timestamp-and-commit tags, and deployed by updating only the relevant service.
The documentation portal is packaged inside the Web image at `/doc/`. It does
not add a public service, port, database, or proxy rule.
## Stable and experimental surfaces
Stable platform contracts:
- authenticated Web-to-Runtime request;
- signed Runtime/Web-to-Gateway identity;
- `ExecutionProvider` workspace semantics;
- per-user volume isolation;
- run-event persistence;
- public model IDs;
- workspace file API.
Expected experiment surfaces:
- prompt and context policy;
- memory selection;
- tool catalog and intent routing;
- scheduler and parallelism;
- delegation;
- recovery and evidence rules;
- model budgets and routing.
Version experiment-sensitive behavior in `run.created` so results remain
comparable after the implementation changes.
+129
View File
@@ -0,0 +1,129 @@
# Development and verification
## Repository layout
```text
agent_platform/
runtime/ Agent API, loop, context, model transport, and tools
gateway/ Authenticated workspace and file service
auth.py Internal service and signed identity verification
models.py Public model catalog and server-side budgets
store.py Run events, memories, and plans
docker/
web/ Open WebUI components and reproducible patch set
*.Dockerfile Web, Runtime, Gateway, and Workspace images
docs/
site/ Static source for /doc/
deploy/ Production Compose files and SSH override
e2e/ Deterministic full-stack verifier and fake provider
scripts/ Verification, evaluation, audit, and secret helpers
tests/ Unit and real Docker integration tests
```
## Local environment
```bash
python3 -m venv .venv
.venv/bin/python -m pip install -e '.[dev]'
cp .env.example .env
./scripts/init-secrets.sh
```
Keep real provider credentials in a protected local environment or deployment
file. Never add them to Git, Docker build arguments, frontend code, test
fixtures, or logs.
Build the user workspace, then start the stack:
```bash
docker compose --profile build-only build workspace-image
docker compose up --build
```
Open <http://localhost:3000>. New registrations have the `pending` role until
approved by the bootstrap administrator.
## Fast feedback
```bash
.venv/bin/ruff check agent_platform tests e2e
.venv/bin/ruff format --check agent_platform tests e2e
.venv/bin/pytest
```
Unit tests are deterministic and do not require a model provider.
## Real Docker integration
```bash
docker build -f docker/workspace.Dockerfile \
-t k1412-agent-workspace:test .
RUN_DOCKER_INTEGRATION=1 \
.venv/bin/pytest tests/test_docker_workspace.py
```
The test creates unique containers, networks, and volumes and removes only
those resources in cleanup.
## Full-stack E2E
```bash
./scripts/verify-e2e.sh
```
The script builds a disposable six-service stack, uses a deterministic model
stub, performs authentication and isolation checks, and removes the stack and
test volumes on exit.
## Live Agent evaluation
```bash
set -a
source /path/to/protected/provider.env
set +a
.venv/bin/python scripts/eval-live-work.py --model deepseek-v4-pro
```
Use `--keep-workspace` only when inspecting a failed case; remove that uniquely
named evaluation workspace afterward.
## Image audit
```bash
./scripts/audit-images.sh
```
The audit requires consistent Python environments and no fixable
High/Critical vulnerabilities in the finished images. Web also receives a
Python dependency audit.
## Documentation portal
`docs/site/` is dependency-free static HTML, CSS, JavaScript, and JSON. It is
copied into `/app/build/doc` by `docker/web.Dockerfile` after Open WebUI's
frontend build completes.
Validate it locally with:
```bash
python3 -m http.server 4173 --directory docs/site
```
The production path is `/doc/`; all internal assets therefore use relative
URLs. Keep the Markdown documents and portal content aligned in the same
commit. Add experiment cards through `docs/site/experiments.json`.
## Change checklist
Before pushing:
1. keep unrelated working-tree changes untouched;
2. update architecture or behavior documentation;
3. add deterministic tests for policy changes;
4. run unit and formatting checks;
5. run Docker integration for workspace/Gateway changes;
6. run E2E for public model, auth, loop, or file-delivery changes;
7. audit every rebuilt image;
8. use immutable production image tags;
9. verify public health and one representative user flow;
10. retain the previous image and deployment backup until verification passes.
+133
View File
@@ -0,0 +1,133 @@
# Agent experiment system
## Goal
The project exists to make Agent-loop iteration cheap, observable, and
reversible. An experiment should change one behavioral hypothesis while
holding authentication, workspace isolation, and delivery infrastructure
constant.
The public portal at <https://agent.k1412.top/doc/#experiments> contains a
curated experiment board. This Markdown document defines the durable process;
the portal catalog is a presentation layer.
## Experiment unit
Every experiment should define:
1. **Question** — the behavior we want to improve.
2. **Hypothesis** — the concrete loop change and expected direction.
3. **Variant identifier** — a stable string recorded in `run.created`.
4. **Task set** — fixed prompts, starting workspace fixtures, and model tier.
5. **Metrics** — success, quality, latency, model/tool usage, and regressions.
6. **Safety constraints** — behavior that must not worsen.
7. **Decision rule** — promote, iterate, or reject.
8. **Artifact** — code commit, configuration, raw event export, and report.
Do not compare variants from different task inputs or silently change the
model, prompt, context budget, or workspace fixture.
## Recommended metrics
Primary:
- task success under explicit evidence checks;
- verified-deliverable rate;
- unresolved tool-failure rate;
- human or rubric quality score.
Efficiency:
- end-to-end latency;
- model calls and iterations;
- prompt/completion tokens when available;
- tool-call count;
- duplicated or policy-blocked calls;
- child-Agent count and concurrency;
- estimated provider and electricity cost.
Reliability and safety:
- incomplete checkpoints correctly rejected;
- false-success rate;
- workspace escape or cross-user access failures;
- non-zero command exits incorrectly recorded as success;
- identity/authentication failures;
- Runtime or workspace restart recovery.
## Evaluation layers
### Unit and policy tests
Fast deterministic tests cover parsing, tool normalization, evidence gates,
authentication, context selection, event persistence, and provider response
normalization.
### Real Docker integration
The integration test creates two disposable users and proves:
- separate containers, networks, and volumes;
- workspace path confinement;
- file browse/download/archive behavior;
- read-only root and dropped capabilities;
- persistent Python virtual environments;
- foreground and background `pipefail` exit codes.
### Disposable full-stack E2E
The E2E stack uses a deterministic fake model provider and exercises:
- registration and admin approval;
- the exact four-model public catalog;
- the custom Agent loop;
- evidence-backed file generation;
- file delivery;
- per-user isolation.
### Live-model evaluation
`scripts/eval-live-work.py` runs a real model against a unique disposable
workspace and records latency, token usage, events, tool counts, and output
files. Live evaluations complement deterministic tests; they do not replace
them.
### Production smoke test
After deployment, use a low-impact authenticated task to confirm public Web,
Runtime, Gateway, the remote workspace host, and file retrieval. Remove only
the smoke artifact after verification.
## Current baselines
| ID | Area | Status | Result |
| --- | --- | --- | --- |
| `loop-v3-evidence` | Completion policy | Production baseline | Artifacts require mutation followed by verification; reports and benchmarks require stronger evidence. |
| `safe-parallel-v1` | Scheduler | Production baseline | Consecutive read-only calls and read-only delegates run concurrently; mutations remain serialized. |
| `recent-visible-v1` | Context | Production baseline | Keep recent visible messages under a per-model character budget and attach up to eight memories. |
| `workspace-python-v1` | Execution | Validated | Persistent `.venv`, persistent user caches, 900-second tool ceiling, and true pipeline exits. |
| `fresh-gateway-identity-v1` | Authentication | Validated | Runtime re-signs verified identity for each tool request so long runs survive the original token expiry. |
## Initial backlog
1. Semantic or hybrid memory retrieval with conflict and expiry policy.
2. Structured old-context summarization with replay comparison.
3. Dependency-aware tool DAG scheduling rather than consecutive safe groups.
4. Child-Agent result contracts and budget allocation.
5. Experiment assignment stored per run and an aggregate comparison API.
6. Cost accounting across local electricity and paid provider tokens.
7. Workspace quotas, egress policy, abuse monitoring, and backup drills.
## Adding an experiment
1. Add the hypothesis and metrics to this document or a dedicated
`docs/experiments/<id>.md` file.
2. Add the experiment to `docs/site/experiments.json` for the public board.
3. Add or update the relevant strategy/scheduler/context identifier in
`run.created`.
4. Add deterministic tests before a live run.
5. Run the standard verification and capture a report.
6. Promote with an immutable image tag; retain the previous tag for rollback.
Never put provider keys, private event payloads, user identifiers, prompts from
real users, or internal credentials in an experiment artifact.
+6
View File
@@ -3,6 +3,7 @@
## Production layout
- Public URL: `https://agent.k1412.top`
- Public documentation: `https://agent.k1412.top/doc/`
- NAS project: `/boot/config/plugins/compose.manager/projects/k1412-agent`
- Public host port: `12004`
- Execution provider: `ssh-docker`
@@ -47,6 +48,11 @@ Compose file, image, repository, or log.
The SSH override removes `/var/run/docker.sock` from Gateway and mounts the
dedicated SSH directory read-only at `/root/.ssh`.
The documentation portal is copied into the existing Web image at
`/app/build/doc`. It uses the same host port and proxy route as Open WebUI, so
it does not require another Compose project, NAS port, DNS record, or Nginx
Proxy Manager host.
The Workspace image keeps the user home, package caches, and Python user base
under `/workspace/.agent`; the conventional project virtual environment is
`/workspace/.venv`. Updating `WORKSPACE_IMAGE` to a new immutable tag causes
+136
View File
@@ -0,0 +1,136 @@
# Project history and decisions
This record summarizes the rebuild that began on 2026-07-26. Git remains the
source of truth for exact changes; this document captures product intent and
architecture decisions that are otherwise difficult to reconstruct from
individual commits.
## 1. Clean-slate web Agent
The existing business/data-specific application was declared a new project
with no backward-compatibility requirement. Business skills, domain workflows,
and user-configurable provider settings were removed from scope. The retained
core requirement was general coding-Agent capability with an independently
evolvable loop.
Decision:
- build one general multi-user Web Agent;
- preserve coding, shell, file, Git, process, planning, memory, and delegation
capabilities;
- keep infrastructure and product shell separate from Agent intelligence.
## 2. Open WebUI as product shell
Instead of maintaining a second full account/chat frontend, Open WebUI v0.9.6
was pinned and lightly patched. It supplies registration, login, RBAC,
administrator approval, conversation history, and familiar chat interaction.
K1412 kept ownership of the Agent loop, provider routing, memory, tools,
scheduling, events, and workspace execution. This reduced the cost of Agent
experiments without making the loop dependent on Open WebUI internals.
## 3. One Agent path, not Chat versus Work
An early design considered a switch between Open WebUI's native Chat loop and
the K1412 Work loop. The product was simplified to one Agent path because:
- users should not need to understand two orchestration engines;
- conversation continuation between modes creates ambiguous state;
- two loops double frontend states and verification behavior;
- the custom loop is the main research asset.
The remaining interface is an Agent-first chat with server-owned model
selection.
## 4. Model and thinking semantics
The original “light / medium / high reasoning” labels conflated different
models with adjustable effort. The UI and backend now distinguish model
identity from thinking capability:
- Luna, Terra, and Sol are separate local K1412/Ollama models;
- each exposes thinking as enabled but without adjustable effort levels;
- DeepSeek V4 Pro is the cloud extreme tier with maximum reasoning effort.
Provider model IDs, URLs, credentials, and tuning remain server-side.
## 5. Multi-user remote workspaces
Each user received a dedicated Docker container, network, and persistent volume
derived from a hashed immutable user ID. Execution moved from the NAS to the
physical `home-node-itx` through SSH Docker.
The physical node was prepared with:
- an ext4 500 GB data disk mounted at `/srv/k1412-data`;
- Docker root under `/srv/k1412-data/docker`;
- strict SSH host verification;
- no application/database secrets.
The local and SSH execution providers retain one interface so the host can be
replaced without changing Agent behavior.
## 6. File delivery and focused UI
The frontend was simplified around the Agent:
- K1412 product title and assets;
- a compact model selector with separate thinking status;
- no exposed provider/tool/system-prompt settings;
- an authenticated workspace file browser;
- single-file download and streamed directory archive.
Open WebUI attribution and license requirements remain documented.
## 7. Loop hardening through failed-task analysis
Real sorting-script/report tasks exposed model weaknesses and platform bugs.
The loop was progressively hardened to:
- require actual files rather than textual claims;
- require executable source separately from reports;
- run the source before writing a report;
- require measured benchmark values;
- reject invalid Python and escaped-layout corruption;
- recover from failed verification;
- prevent unchanged retry loops and duplicate batches;
- preserve failures even when output is piped through another command;
- bound iteration, tool-batch, model-output, and retry budgets.
These constraints are not assumed permanently optimal. They are the current
baseline and should be removed or changed only through measured experiments.
## 8. Python environment and long-run reliability
Production debugging found three platform problems:
1. the container root was read-only while `pip` defaulted to `/home/agent`;
2. `/tmp` was `noexec`;
3. shell pipelines returned the last command's status, hiding failed installs.
The fix moved home, caches, user packages, and virtual environments to the
persistent workspace, enabled Bash `pipefail`, extended tool timeouts to 900
seconds, and taught the Agent the supported `.venv` workflow.
The same investigation found that a five-minute identity token was reused for
every tool in a long run. Runtime now signs a fresh short-lived identity for
each Gateway call after the original request identity has been verified.
Production validation created a real `.venv`, installed and imported
`psutil 7.2.2`, downloaded the generated proof file through the Web API, and
then removed the temporary proof file.
## 9. Documentation and experiment portal
The repository documentation was reorganized around architecture, Agent
implementation, security, operations, history, development, and experiments.
The curated public portal is shipped inside the existing Web image at
`/doc/`, avoiding a second service or proxy rule.
## Current direction
The platform is now suitable for controlled Agent-loop experimentation. The
next work should prioritize evaluation data, context/memory experiments,
scheduler design, delegation contracts, cost accounting, and hardening for
hostile multi-tenant use rather than adding business-specific workflows.
+265
View File
@@ -0,0 +1,265 @@
const flows = {
agent: {
kicker: 'AGENT REQUEST / 7 HOPS',
description:
'浏览器的已登录请求由 Web 转换成兼容 OpenAI 的流式调用;Runtime 独立完成上下文构建、模型调用、工具循环和证据校验。',
nodes: ['browser', 'web', 'runtime', 'provider', 'gateway', 'workspace', 'store']
},
identity: {
kicker: 'IDENTITY / SHORT-LIVED & SERVER-SIDE',
description:
'Web 先验证登录用户并签发短期身份;Runtime 验证一次后,在每次 Gateway 工具调用前重新签发,避免长任务复用过期令牌。',
nodes: ['browser', 'web', 'runtime', 'gateway', 'workspace']
},
tool: {
kicker: 'TOOL EXECUTION / POLICY BEFORE DOCKER',
description:
'Runtime 校验并调度工具,Gateway 再执行身份、路径和资源策略,最后才通过 SSH Docker 进入该用户唯一的工作区。',
nodes: ['runtime', 'gateway', 'workspace', 'store']
},
file: {
kicker: 'FILE DELIVERY / AUTHENTICATED STREAM',
description:
'用户在工作区抽屉发起浏览或下载;Web 验证会话后请求 Gateway,文件或归档以流式响应返回,浏览器从不接触内部服务密钥。',
nodes: ['browser', 'web', 'gateway', 'workspace']
}
};
const stages = {
context: {
kicker: 'CONTEXT / recent-visible-v1',
title: '把有限上下文留给真正影响下一步的内容',
description:
'移除旧的工具详情渲染,按模型预算保留最新可见消息,再注入最多 8 条持久记忆。上下文策略和 Agent Loop 分别版本化。',
points: [
'去掉历史消息中的工具 UI 标记',
'最近消息优先,按字符预算截断',
'记忆只作为补充上下文,不直接取得执行权限'
]
},
model: {
kicker: 'MODEL / SERVER-SIDE CATALOG',
title: '模型提出下一步,平台控制它能看到和能花费什么',
description:
'Runtime 根据公开模型 ID 选择固定 provider、上下文预算、输出预算和循环上限。用户不能从前端注入密钥或任意模型参数。',
points: ['支持流式内容与工具调用', '模型和推理强度明确标注', 'provider 差异在传输层统一']
},
validate: {
kicker: 'VALIDATE / NORMALIZE BEFORE TRUST',
title: '先把模型输出变成可约束的数据,再决定是否执行',
description:
'工具名称、参数、批次数量和权限都在执行前检查。一次模型响应最多接受 8 个工具调用,未知或畸形调用会成为可见错误。',
points: ['JSON 参数规范化', '工具白名单与参数校验', '重复无进展调用守卫']
},
schedule: {
kicker: 'SCHEDULER / safe-parallel-v1',
title: '读取可以加速,写入必须保持可预测',
description:
'连续且标记为 parallel_safe 的工具并发执行;任何文件写入、命令或状态变更都会形成串行边界,结果按请求顺序返回模型。',
points: ['只读调用成组并发', '变更操作严格串行', '批次结果仍保持稳定顺序']
},
execute: {
kicker: 'EXECUTE / TOOLS + BOUNDED DELEGATION',
title: '所有能力都通过可记录、可验证的工具边界发生',
description:
'文件、命令、Git、计划和记忆都有结构化工具。Workspace 工具经 Gateway 进入用户容器;只读子任务可以受限委派。',
points: ['工作区工具与状态工具分离', '每次调用记录开始和结果事件', '子 Agent 不得再次委派']
},
evidence: {
kicker: 'EVIDENCE / COMPLETION IS A POLICY',
title: '“完成”不是一句自然语言,而是一组已经发生的事实',
description:
'如果任务要求生成文件、修改代码或给出测量报告,循环必须看到相应的写入、验证和精确结果。证据不足时会继续循环或明确失败。',
points: ['变更后必须有后续验证', '失败命令必须修复并重跑', '报告必须晚于来源或实验结果']
}
};
const statusLabels = {
baseline: '当前基线',
validated: '已验证',
backlog: '待实验'
};
let experimentData = [];
let activeExperimentFilter = 'all';
function activateFlow(flowName) {
const flow = flows[flowName];
if (!flow) return;
document.querySelectorAll('.flow-tab').forEach((button) => {
const active = button.dataset.flow === flowName;
button.classList.toggle('is-active', active);
button.setAttribute('aria-selected', String(active));
});
document.querySelectorAll('.system-node').forEach((node) => {
node.classList.toggle('is-flow-active', flow.nodes.includes(node.dataset.node));
});
document.querySelector('.system-map')?.classList.add('has-selection');
document.querySelector('#flow-kicker').textContent = flow.kicker;
document.querySelector('#flow-description').textContent = flow.description;
}
function activateStage(stageName) {
const stage = stages[stageName];
if (!stage) return;
document.querySelectorAll('.loop-stage').forEach((button) => {
const active = button.dataset.stage === stageName;
button.classList.toggle('is-active', active);
button.setAttribute('aria-selected', String(active));
});
document.querySelector('#stage-kicker').textContent = stage.kicker;
document.querySelector('#stage-title').textContent = stage.title;
document.querySelector('#stage-description').textContent = stage.description;
const list = document.querySelector('#stage-points');
list.replaceChildren(
...stage.points.map((point) => {
const item = document.createElement('li');
item.textContent = point;
return item;
})
);
}
function experimentCard(experiment) {
const article = document.createElement('article');
article.className = 'experiment-card';
const meta = document.createElement('div');
meta.className = 'experiment-meta';
const id = document.createElement('span');
id.textContent = experiment.id;
const badge = document.createElement('span');
badge.className = `status-badge status-${experiment.status}`;
badge.textContent = statusLabels[experiment.status];
meta.append(id, badge);
const title = document.createElement('h3');
title.textContent = experiment.title;
const summary = document.createElement('p');
summary.textContent = experiment.summary;
const footer = document.createElement('div');
footer.className = 'experiment-footer';
[experiment.area, experiment.version, ...(experiment.metrics || [])].filter(Boolean).forEach((text) => {
const tag = document.createElement('span');
tag.textContent = text;
footer.append(tag);
});
article.append(meta, title, summary, footer);
return article;
}
function renderExperiments() {
const query = document.querySelector('#experiment-search').value.trim().toLocaleLowerCase('zh-CN');
const filtered = experimentData.filter((experiment) => {
const statusMatches =
activeExperimentFilter === 'all' || experiment.status === activeExperimentFilter;
const haystack = [
experiment.id,
experiment.title,
experiment.summary,
experiment.area,
experiment.version,
...(experiment.metrics || [])
]
.join(' ')
.toLocaleLowerCase('zh-CN');
return statusMatches && (!query || haystack.includes(query));
});
const grid = document.querySelector('#experiment-grid');
if (filtered.length === 0) {
const empty = document.createElement('article');
empty.className = 'experiment-empty';
empty.textContent = '没有匹配的实验记录。';
grid.replaceChildren(empty);
} else {
grid.replaceChildren(...filtered.map(experimentCard));
}
document.querySelector('#experiment-count').textContent =
`${String(filtered.length).padStart(2, '0')} / ${String(experimentData.length).padStart(2, '0')} RECORDS`;
}
async function loadExperiments() {
const grid = document.querySelector('#experiment-grid');
try {
const response = await fetch('./experiments.json', { cache: 'no-cache' });
if (!response.ok) throw new Error(`HTTP ${response.status}`);
experimentData = await response.json();
renderExperiments();
} catch (error) {
const failure = document.createElement('article');
failure.className = 'experiment-empty';
failure.textContent = '实验台账暂时无法读取,请查看仓库中的 docs/experiments.md。';
grid.replaceChildren(failure);
document.querySelector('#experiment-count').textContent = 'DATA UNAVAILABLE';
}
}
function setActiveNavigation(sectionId) {
document.querySelectorAll('.nav-link').forEach((link) => {
link.classList.toggle('is-active', link.getAttribute('href') === `#${sectionId}`);
});
}
document.querySelectorAll('.flow-tab').forEach((button) => {
button.addEventListener('click', () => activateFlow(button.dataset.flow));
});
document.querySelectorAll('.loop-stage').forEach((button) => {
button.addEventListener('click', () => activateStage(button.dataset.stage));
});
document.querySelectorAll('.filter-button').forEach((button) => {
button.addEventListener('click', () => {
activeExperimentFilter = button.dataset.filter;
document
.querySelectorAll('.filter-button')
.forEach((candidate) => candidate.classList.toggle('is-active', candidate === button));
renderExperiments();
});
});
document.querySelector('#experiment-search').addEventListener('input', renderExperiments);
const toggle = document.querySelector('.nav-toggle');
const sidebar = document.querySelector('.sidebar');
toggle.addEventListener('click', () => {
const isOpen = sidebar.classList.toggle('is-open');
toggle.setAttribute('aria-expanded', String(isOpen));
});
document.querySelectorAll('.nav-link').forEach((link) => {
link.addEventListener('click', () => {
sidebar.classList.remove('is-open');
toggle.setAttribute('aria-expanded', 'false');
});
});
if ('IntersectionObserver' in window) {
const observer = new IntersectionObserver(
(entries) => {
const visible = entries
.filter((entry) => entry.isIntersecting)
.sort((a, b) => b.intersectionRatio - a.intersectionRatio);
if (visible[0]) setActiveNavigation(visible[0].target.id);
},
{ rootMargin: '-20% 0px -62% 0px', threshold: [0.05, 0.3] }
);
document.querySelectorAll('main > section[id]').forEach((section) => observer.observe(section));
}
activateFlow('agent');
activateStage('context');
loadExperiments();
+92
View File
@@ -0,0 +1,92 @@
[
{
"id": "BASE-001",
"title": "统一 Agent 路径",
"summary": "删除 Chat / Work 双模式,所有对话通过同一套可观测 Agent Loop;减少前端状态和后端分叉。",
"area": "product",
"status": "validated",
"version": "agent-loop-v3",
"metrics": ["维护面", "交互一致性"]
},
{
"id": "BASE-002",
"title": "安全读取并行,变更串行",
"summary": "连续 parallel_safe 调用并发执行,写文件、命令和状态变更维持模型给出的顺序。",
"area": "scheduler",
"status": "baseline",
"version": "safe-parallel-v1",
"metrics": ["wall time", "determinism"]
},
{
"id": "BASE-003",
"title": "最近可见上下文",
"summary": "清除旧工具详情,按模型字符预算保留最近消息,并注入最多 8 条持久记忆。",
"area": "context",
"status": "baseline",
"version": "recent-visible-v1",
"metrics": ["token proxy", "task success"]
},
{
"id": "BASE-004",
"title": "证据驱动的完成判定",
"summary": "文件、代码和报告任务必须出现写入与后续验证;未解决的命令失败会阻止虚假完成。",
"area": "reliability",
"status": "validated",
"version": "evidence-gate-v1",
"metrics": ["false completion", "artifact validity"]
},
{
"id": "BASE-005",
"title": "持久 Python 工具链",
"summary": "用户依赖安装到 /workspace/.venv;镜像升级保留环境,pipefail 防止安装失败被管道尾部掩盖。",
"area": "workspace",
"status": "validated",
"version": "workspace-python-v2",
"metrics": ["install success", "restart survival"]
},
{
"id": "EXP-006",
"title": "基于相关性的记忆召回",
"summary": "比较固定最近记忆、关键词召回和向量召回对长期偏好遵循、上下文成本与错误注入的影响。",
"area": "memory",
"status": "backlog",
"version": "proposal",
"metrics": ["recall@k", "task success", "context cost"]
},
{
"id": "EXP-007",
"title": "依赖感知工具调度",
"summary": "从显式工具属性推进到调用间读写集分析,评估是否能扩大安全并行范围。",
"area": "scheduler",
"status": "backlog",
"version": "proposal",
"metrics": ["wall time", "race failures", "parallel ratio"]
},
{
"id": "EXP-008",
"title": "自适应模型路由",
"summary": "以任务复杂度和中途失败信号决定 Luna、Terra、Sol 或 DeepSeek,而不是让一次选择锁定整个任务。",
"area": "model-routing",
"status": "backlog",
"version": "proposal",
"metrics": ["task success", "latency", "API cost"]
},
{
"id": "EXP-009",
"title": "受限多 Agent 拓扑",
"summary": "比较单循环、只读并行委派和角色化子 Agent,在复杂代码任务中的收益与上下文整合成本。",
"area": "delegation",
"status": "backlog",
"version": "proposal",
"metrics": ["task success", "wall time", "tool calls"]
},
{
"id": "EXP-010",
"title": "上下文压缩与可逆摘要",
"summary": "在最近消息预算之外保存结构化任务状态,并允许模型按需取回原始证据,减少长任务遗忘。",
"area": "context",
"status": "backlog",
"version": "proposal",
"metrics": ["long-horizon success", "context cost", "retrieval count"]
}
]
+570
View File
@@ -0,0 +1,570 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#f2f0e9" />
<title>K1412 Agent · 架构、实现与实验</title>
<meta
name="description"
content="K1412 多用户 Web Agent 的架构设计、Agent Loop 实现、隔离模型和实验记录。"
/>
<meta property="og:type" content="website" />
<meta property="og:site_name" content="K1412 Agent" />
<meta property="og:title" content="K1412 Agent · Architecture & Experiments" />
<meta
property="og:description"
content="A multi-user Web Agent built as an evolvable system for context, tools, parallelism, memory and delegation."
/>
<meta property="og:url" content="https://agent.k1412.top/doc/" />
<meta property="og:image" content="https://agent.k1412.top/doc/og.png" />
<link rel="icon" href="../static/favicon.svg" type="image/svg+xml" />
<link rel="stylesheet" href="./styles.css" />
<script>
document.documentElement.classList.add('js');
</script>
<script src="./app.js" defer></script>
</head>
<body>
<a class="skip-link" href="#main-content">跳到正文</a>
<header class="topbar">
<a class="brand" href="#overview" aria-label="K1412 Agent 文档首页">
<span class="brand-mark" aria-hidden="true">K</span>
<span>
<strong>K1412 Agent</strong>
<small>Architecture / Lab Notes</small>
</span>
</a>
<div class="topbar-actions">
<span class="system-status"><i></i> 当前架构 · 2026.07</span>
<a class="button button-quiet hide-mobile" href="https://git.k1412.top/wuyang/zk-data-agent">
查看源码
</a>
<a class="button button-primary" href="/">进入 Agent <span aria-hidden="true"></span></a>
<button
class="nav-toggle"
type="button"
aria-label="打开文档目录"
aria-expanded="false"
aria-controls="side-navigation"
>
<span></span><span></span>
</button>
</div>
</header>
<div class="page-shell">
<aside class="sidebar" id="side-navigation">
<nav aria-label="文档目录">
<p class="nav-label">系统</p>
<a class="nav-link is-active" href="#overview"><span>01</span>概览</a>
<a class="nav-link" href="#architecture"><span>02</span>架构边界</a>
<a class="nav-link" href="#agent-loop"><span>03</span>Agent Loop</a>
<a class="nav-link" href="#workspace"><span>04</span>执行空间</a>
<a class="nav-link" href="#models"><span>05</span>模型与强度</a>
<p class="nav-label">演进</p>
<a class="nav-link" href="#experiments"><span>06</span>实验记录</a>
<a class="nav-link" href="#operations"><span>07</span>部署与观测</a>
<a class="nav-link" href="#timeline"><span>08</span>项目历程</a>
<a class="nav-link" href="#repository"><span>09</span>仓库文档</a>
</nav>
<div class="sidebar-note">
<span>核心原则</span>
<p>稳定的平台外壳,快速演进的 Agent 核心。</p>
</div>
</aside>
<main id="main-content">
<section class="hero section" id="overview">
<div class="eyebrow"><span>ONE AGENT PATH</span> / MULTI-USER / SELF-HOSTED</div>
<h1>一套可以被持续<br />实验的 Web Agent</h1>
<p class="hero-copy">
K1412 Agent 把账户、界面和执行隔离做成稳定平台,把上下文、工具调度、并行、多 Agent、
记忆和完成判定留在我们自己的 Agent Loop 中。这里既是系统说明,也是后续实验的公开台账。
</p>
<div class="hero-actions">
<a class="button button-dark" href="#architecture">理解架构 <span aria-hidden="true"></span></a>
<a class="text-link" href="#experiments">查看实验记录 <span aria-hidden="true"></span></a>
</div>
<div class="metric-strip" aria-label="当前系统摘要">
<div>
<strong>01</strong>
<span>统一 Agent 路径</span>
<small>没有 Chat / Work 分叉</small>
</div>
<div>
<strong>04</strong>
<span>可选模型</span>
<small>模型与推理强度分离</small>
</div>
<div>
<strong>1 : 1</strong>
<span>用户执行空间</span>
<small>容器、网络与持久卷</small>
</div>
<div>
<strong>SSE</strong>
<span>运行事件流</span>
<small>模型、工具与完成证据</small>
</div>
</div>
</section>
<section class="section architecture-section" id="architecture">
<div class="section-heading">
<div>
<span class="section-index">02 / ARCHITECTURE</span>
<h2>稳定外壳与<br />实验核心解耦</h2>
</div>
<p>
点击一条链路,观察一次请求如何穿过系统。公开入口只有 Web;模型密钥、Docker
权限和用户执行环境始终留在服务端。
</p>
</div>
<div class="architecture-lab">
<div class="flow-tabs" role="tablist" aria-label="架构链路">
<button class="flow-tab is-active" type="button" role="tab" data-flow="agent">
Agent 请求
</button>
<button class="flow-tab" type="button" role="tab" data-flow="identity">身份链路</button>
<button class="flow-tab" type="button" role="tab" data-flow="tool">工具执行</button>
<button class="flow-tab" type="button" role="tab" data-flow="file">文件交付</button>
</div>
<div class="system-map" aria-label="K1412 Agent 系统架构图">
<div class="map-lane map-public">
<span class="lane-label">PUBLIC EDGE</span>
<article class="system-node" data-node="browser">
<small>01 / CLIENT</small>
<strong>Browser</strong>
<span>登录、会话、文件下载</span>
</article>
<span class="map-arrow" aria-hidden="true"></span>
<article class="system-node node-web" data-node="web">
<small>02 / STABLE SHELL</small>
<strong>Open WebUI</strong>
<span>Auth · RBAC · History · UI</span>
</article>
</div>
<div class="map-lane map-core">
<span class="lane-label">PRIVATE APPLICATION NETWORK</span>
<article class="system-node node-runtime" data-node="runtime">
<small>03 / EXPERIMENT CORE</small>
<strong>Agent Runtime</strong>
<span>Context · Loop · Tools · Memory</span>
</article>
<span class="map-arrow" aria-hidden="true"></span>
<article class="system-node" data-node="provider">
<small>04 / INFERENCE</small>
<strong>Model Provider</strong>
<span>K1412 API · DeepSeek</span>
</article>
<article class="system-node" data-node="gateway">
<small>05 / POLICY GATE</small>
<strong>Workspace Gateway</strong>
<span>Identity · Path · Docker lifecycle</span>
</article>
<article class="system-node node-store" data-node="store">
<small>STATE</small>
<strong>PostgreSQL + Redis</strong>
<span>Chats · Events · Plans · Memory</span>
</article>
</div>
<div class="map-lane map-execution">
<span class="lane-label">TAILSCALE / PHYSICAL WORKER</span>
<article class="system-node node-workspace" data-node="workspace">
<small>06 / USER BOUNDARY</small>
<strong>Per-user Workspace</strong>
<span>Container · Network · Persistent volume</span>
</article>
</div>
</div>
<div class="flow-explainer" aria-live="polite">
<span id="flow-kicker">AGENT REQUEST / 7 HOPS</span>
<p id="flow-description">
浏览器的已登录请求由 Web 转换成兼容 OpenAI 的流式调用;Runtime
独立完成上下文构建、模型调用、工具循环和证据校验。
</p>
</div>
</div>
<div class="boundary-grid">
<article>
<span class="boundary-number">A</span>
<h3>Open WebUI</h3>
<p>负责用户系统、会话、历史和呈现。它是产品外壳,不决定 Agent 如何思考与执行。</p>
<small>STABLE / UPSTREAM-BASED</small>
</article>
<article>
<span class="boundary-number">B</span>
<h3>Agent Runtime</h3>
<p>负责模型、上下文、工具、并行、委派、记忆和完成判定,是我们主要的实验面。</p>
<small>FAST-MOVING / OWNED</small>
</article>
<article>
<span class="boundary-number">C</span>
<h3>Gateway + Workspace</h3>
<p>把已验证用户绑定到唯一执行空间,控制路径、资源和 Docker 生命周期。</p>
<small>SECURITY BOUNDARY</small>
</article>
</div>
</section>
<section class="section loop-section" id="agent-loop">
<div class="section-heading inverse-heading">
<div>
<span class="section-index">03 / AGENT LOOP</span>
<h2>模型不是 Agent<br />循环才是。</h2>
</div>
<p>
当前基线为 <code>agent-loop-v3</code>。模型每轮可以回答或提出工具调用;Runtime
负责校验、调度、恢复,并拒绝没有证据的“已完成”。
</p>
</div>
<div class="loop-console">
<div class="loop-stages" role="tablist" aria-label="Agent Loop 阶段">
<button class="loop-stage is-active" type="button" data-stage="context">
<span>01</span><strong>Context</strong><small>裁剪与记忆</small>
</button>
<button class="loop-stage" type="button" data-stage="model">
<span>02</span><strong>Model</strong><small>生成意图</small>
</button>
<button class="loop-stage" type="button" data-stage="validate">
<span>03</span><strong>Validate</strong><small>规范与限制</small>
</button>
<button class="loop-stage" type="button" data-stage="schedule">
<span>04</span><strong>Schedule</strong><small>并行与串行</small>
</button>
<button class="loop-stage" type="button" data-stage="execute">
<span>05</span><strong>Execute</strong><small>工具与委派</small>
</button>
<button class="loop-stage" type="button" data-stage="evidence">
<span>06</span><strong>Evidence</strong><small>验证与完成</small>
</button>
</div>
<div class="loop-detail">
<div>
<span class="detail-label" id="stage-kicker">CONTEXT / recent-visible-v1</span>
<h3 id="stage-title">把有限上下文留给真正影响下一步的内容</h3>
</div>
<p id="stage-description">
移除旧的工具详情渲染,按模型预算保留最新可见消息,再注入最多 8
条持久记忆。上下文策略和 Agent Loop 分别版本化。
</p>
<ul id="stage-points">
<li>去掉历史消息中的工具 UI 标记</li>
<li>最近消息优先,按字符预算截断</li>
<li>记忆只作为补充上下文,不直接取得执行权限</li>
</ul>
</div>
</div>
<div class="loop-principles">
<article>
<span>PARALLELISM</span>
<h3>只并行已知安全的读取</h3>
<p>
连续的只读、<code>parallel_safe</code> 工具可以并发;写入和状态变更保持顺序,
避免输出更快但结果不可复现。
</p>
</article>
<article>
<span>RECOVERY</span>
<h3>失败不是上下文噪声</h3>
<p>命令失败必须被修复并重新运行,重复无进展调用会被守卫阻止,模型不能用文字绕过失败。</p>
</article>
<article>
<span>DELEGATION</span>
<h3>委派是受限的工具</h3>
<p>只读子任务可并行,子 Agent 不能继续委派。父循环仍然负责整合证据和最终完成判定。</p>
</article>
</div>
<div class="evidence-table" aria-label="完成证据规则">
<div class="table-row table-head">
<span>声称完成的类型</span><span>最低证据</span><span>阻止条件</span>
</div>
<div class="table-row">
<strong>生成文件</strong><span>成功写入 + 随后读取或列出</span><span>只有模型文字</span>
</div>
<div class="table-row">
<strong>修改代码</strong><span>变更操作 + 相关验证</span><span>未解决的失败命令</span>
</div>
<div class="table-row">
<strong>生成报告</strong><span>来源或执行结果 + 报告文件验证</span><span>报告早于实验</span>
</div>
<div class="table-row">
<strong>性能比较</strong><span>可解析的精确数值</span><span>只有定性结论</span>
</div>
</div>
</section>
<section class="section workspace-section" id="workspace">
<div class="section-heading">
<div>
<span class="section-index">04 / EXECUTION SPACE</span>
<h2>每个用户,一套<br />长期存在的工具台</h2>
</div>
<p>
生产执行发生在物理机 <code>home-node-itx</code>。Gateway 通过 Tailscale + SSH
管理 Docker;用户得到独立容器、网络和持久卷,而不是宿主机账号。
</p>
</div>
<div class="isolation-visual">
<div class="identity-card">
<small>IMMUTABLE INPUT</small>
<strong>Open WebUI user_id</strong>
<code>sha256(user_id) → workspace_id</code>
</div>
<div class="identity-arrow" aria-hidden="true"></div>
<div class="workspace-stack">
<div><span>CONTAINER</span><code>k1412-ws-&lt;id&gt;</code></div>
<div><span>NETWORK</span><code>k1412-ws-net-&lt;id&gt;</code></div>
<div><span>VOLUME</span><code>k1412-ws-data-&lt;id&gt;</code></div>
</div>
</div>
<div class="security-grid">
<article>
<span class="security-icon">01</span>
<h3>身份不下放给浏览器</h3>
<p>Web 验证登录,Runtime 每次调用 Gateway 前重新签发短期身份。内部服务密钥从不进入前端。</p>
</article>
<article>
<span class="security-icon">02</span>
<h3>容器不是主机边界的替代品</h3>
<p>只读根目录、丢弃 capabilities、no-new-privileges、资源限制、无宿主端口和 Docker socket。</p>
</article>
<article>
<span class="security-icon">03</span>
<h3>依赖和文件都会留下</h3>
<p><code>/workspace</code> 由命名卷持久化;Python 位于 <code>.venv</code>,镜像升级不会删除用户数据。</p>
</article>
<article>
<span class="security-icon">04</span>
<h3>输出可以直接取回</h3>
<p>工作区抽屉支持浏览、下载文件和流式归档。用户无需接触 Gateway、SSH 或容器名称。</p>
</article>
</div>
</section>
<section class="section models-section" id="models">
<div class="section-heading">
<div>
<span class="section-index">05 / MODEL POLICY</span>
<h2>先标清模型,<br />再标真实思考能力</h2>
</div>
<p>
Luna、Terra、Sol 是三个独立模型,不是同一模型的轻、中、高强度。前端只显示后端确认支持的
思考语义;provider、循环和上下文预算仍由服务端模型目录固定。
</p>
</div>
<div class="model-grid">
<article class="model-card">
<div><span class="model-dot luna"></span><small>LOCAL MODEL</small></div>
<h3>Luna</h3>
<p>思考:开启(不分档)</p>
<dl><div><dt>循环上限</dt><dd>8</dd></div><div><dt>上下文预算</dt><dd>120k chars</dd></div></dl>
</article>
<article class="model-card is-default">
<div><span class="model-dot terra"></span><small>LOCAL / DEFAULT</small></div>
<h3>Terra</h3>
<p>思考:开启(不分档)</p>
<dl><div><dt>循环上限</dt><dd>16</dd></div><div><dt>上下文预算</dt><dd>240k chars</dd></div></dl>
</article>
<article class="model-card">
<div><span class="model-dot sol"></span><small>LOCAL MODEL</small></div>
<h3>Sol</h3>
<p>思考:开启(不分档)</p>
<dl><div><dt>循环上限</dt><dd>24</dd></div><div><dt>上下文预算</dt><dd>400k chars</dd></div></dl>
</article>
<article class="model-card model-deepseek">
<div><span class="model-dot deepseek"></span><small>CLOUD / MAX EFFORT</small></div>
<h3>DeepSeek V4 Pro</h3>
<p>思考强度:极高(max</p>
<dl><div><dt>循环上限</dt><dd>32</dd></div><div><dt>上下文预算</dt><dd>800k chars</dd></div></dl>
</article>
</div>
<div class="policy-note">
<span>命名规则</span>
<p>
<strong>Luna / Terra / Sol / DeepSeek V4 Pro</strong> 是四个独立模型。前三者目前只暴露
“思考开启”的布尔能力,不能严谨地标成轻、中、高;DeepSeek 才明确使用
<strong>reasoning_effort=max</strong>,界面标注为“极高”。
</p>
</div>
</section>
<section class="section experiments-section" id="experiments">
<div class="section-heading">
<div>
<span class="section-index">06 / EXPERIMENT LOG</span>
<h2>每次改进都应该<br />留下可比较的记录</h2>
</div>
<p>
实验以问题、基线、变量、数据集、指标和结论为最小单元。页面读取仓库里的
<code>experiments.json</code>,与实现一起版本化。
</p>
</div>
<div class="experiment-toolbar">
<div class="filter-group" role="group" aria-label="实验状态筛选">
<button class="filter-button is-active" type="button" data-filter="all">全部</button>
<button class="filter-button" type="button" data-filter="baseline">基线</button>
<button class="filter-button" type="button" data-filter="validated">已验证</button>
<button class="filter-button" type="button" data-filter="backlog">待实验</button>
</div>
<label class="experiment-search">
<span class="sr-only">搜索实验</span>
<input id="experiment-search" type="search" placeholder="搜索上下文、并行、记忆…" />
<span aria-hidden="true"></span>
</label>
</div>
<div class="experiment-grid" id="experiment-grid" aria-live="polite">
<article class="experiment-loading">正在读取实验台账…</article>
</div>
<p class="experiment-count" id="experiment-count"></p>
</section>
<section class="section operations-section" id="operations">
<div class="section-heading inverse-heading">
<div>
<span class="section-index">07 / OPERATIONS</span>
<h2>一条公开入口,<br />多层私有服务</h2>
</div>
<p>
生产栈运行于 Unraid NAS,公开流量经 VPS 上的 Nginx Proxy Manager 和 Tailscale
进入 NAS。文档和聊天由同一个 Web 镜像提供。
</p>
</div>
<div class="deployment-path" aria-label="部署链路">
<div><small>GIT</small><strong>git.k1412.top</strong></div>
<span></span>
<div><small>OCI</small><strong>docker.k1412.top</strong></div>
<span></span>
<div><small>COMPOSE</small><strong>Unraid NAS :12004</strong></div>
<span></span>
<div><small>PUBLIC</small><strong>agent.k1412.top</strong></div>
</div>
<div class="ops-grid">
<article>
<span>01 / BUILD</span>
<h3>不可变镜像</h3>
<p>镜像按 UTC 时间与 Git commit 标记;发布只替换发生变化的服务。</p>
</article>
<article>
<span>02 / VERIFY</span>
<h3>分层验证</h3>
<p>静态检查、单元测试、真实 Docker、完整 E2E、镜像审计与公开路径冒烟。</p>
</article>
<article>
<span>03 / OBSERVE</span>
<h3>事件优先</h3>
<p>每轮上下文、模型、工具和完成状态进入运行事件,便于重放与实验比较。</p>
</article>
<article>
<span>04 / ROLLBACK</span>
<h3>保留上个版本</h3>
<p>配置先备份,旧镜像不覆盖;验证失败时恢复镜像引用即可回退。</p>
</article>
</div>
</section>
<section class="section timeline-section" id="timeline">
<div class="section-heading">
<div>
<span class="section-index">08 / PROJECT HISTORY</span>
<h2>从业务 Agent 到<br />通用实验平台</h2>
</div>
<p>
项目按“干净重建”推进,不保留旧业务 Skill 和旧 API 兼容层。下面记录已经改变系统性质的关键决策。
</p>
</div>
<ol class="timeline">
<li>
<span>01</span>
<div><small>FOUNDATION</small><h3>重建为多人 Web Agent</h3><p>Open WebUI 承担账户和聊天外壳,自研 Runtime 承担 Agent 能力。</p></div>
</li>
<li>
<span>02</span>
<div><small>PRODUCT</small><h3>收敛为单一 Agent 路径</h3><p>删除 Chat / Work 双模式,降低交互和后端维护成本。</p></div>
</li>
<li>
<span>03</span>
<div><small>EXECUTION</small><h3>迁移到远端持久工作区</h3><p>接入 home-node-itx,打通每用户容器、持久卷与文件交付。</p></div>
</li>
<li>
<span>04</span>
<div><small>RELIABILITY</small><h3>建立证据驱动的完成策略</h3><p>加入恢复、重试守卫、精确报告证据和结构化事件。</p></div>
</li>
<li>
<span>05</span>
<div><small>PLATFORM</small><h3>补齐 Python 环境与长期身份</h3><p>持久化虚拟环境、pipefail、900 秒工具超时和每次调用的新鲜身份令牌。</p></div>
</li>
<li class="is-current">
<span>06</span>
<div><small>NOW</small><h3>文档与实验成为产品界面</h3><p>架构、实现、运维和实验台账随代码发布到同域门户。</p></div>
</li>
</ol>
</section>
<section class="section repository-section" id="repository">
<div class="repository-panel">
<div>
<span class="section-index">09 / REPOSITORY</span>
<h2>网页负责建立心智模型,<br />仓库文档负责成为事实来源。</h2>
<p>
所有设计说明、限制、运维步骤和实验规范都与代码一同提交。门户是摘要,
Markdown 文档是评审与变更时的正式依据。
</p>
<a class="button button-light" href="https://git.k1412.top/wuyang/zk-data-agent">
打开 Git 仓库 <span aria-hidden="true"></span>
</a>
</div>
<div class="doc-list">
<a href="https://git.k1412.top/wuyang/zk-data-agent">
<span>README.md</span><small>产品入口与快速开始</small>
</a>
<a href="https://git.k1412.top/wuyang/zk-data-agent">
<span>docs/architecture.md</span><small>系统边界与请求生命周期</small>
</a>
<a href="https://git.k1412.top/wuyang/zk-data-agent">
<span>docs/agent-loop.md</span><small>Loop、调度、证据与委派</small>
</a>
<a href="https://git.k1412.top/wuyang/zk-data-agent">
<span>docs/experiments.md</span><small>实验方法与路线图</small>
</a>
<a href="https://git.k1412.top/wuyang/zk-data-agent">
<span>docs/operations.md</span><small>发布、恢复与故障处理</small>
</a>
<a href="https://git.k1412.top/wuyang/zk-data-agent">
<span>docs/security.md</span><small>威胁边界与安全控制</small>
</a>
</div>
</div>
</section>
</main>
</div>
<footer>
<span>K1412 AGENT / SYSTEM NOTES</span>
<p>代码、文档与实验共同演进。</p>
<a href="#overview">回到顶部 ↑</a>
</footer>
</body>
</html>
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

+1688
View File
File diff suppressed because it is too large Load Diff
+23 -2
View File
@@ -149,6 +149,27 @@ def main() -> None:
health.raise_for_status()
assert health.json().get("status") is True
docs_entry = client.get("/doc", follow_redirects=False)
assert docs_entry.status_code in {307, 308}
assert docs_entry.headers["location"].endswith("/doc/")
docs = client.get("/doc/")
docs.raise_for_status()
assert "K1412 Agent · 架构、实现与实验" in docs.text
assert docs.headers["content-type"].startswith("text/html")
docs_style = client.get("/doc/styles.css")
docs_style.raise_for_status()
assert "--green: #b8f23d" in docs_style.text
docs_script = client.get("/doc/app.js")
docs_script.raise_for_status()
assert "safe-parallel-v1" in docs_script.text
docs_experiments = client.get("/doc/experiments.json")
docs_experiments.raise_for_status()
assert {item["status"] for item in docs_experiments.json()} == {
"baseline",
"validated",
"backlog",
}
admin = _signin(client, ADMIN_EMAIL, ADMIN_PASSWORD)
admin_token = admin["token"]
@@ -276,8 +297,8 @@ def main() -> None:
assert legacy.status_code == 404, legacy.text
print(
"E2E passed: auth approval, four Agent models, custom loop evidence, file downloads, "
"and per-user workspace isolation."
"E2E passed: public documentation, auth approval, four Agent models, custom loop "
"evidence, file downloads, and per-user workspace isolation."
)
+90
View File
@@ -0,0 +1,90 @@
from __future__ import annotations
import json
from html.parser import HTMLParser
from pathlib import Path
from urllib.parse import urlparse
SITE_ROOT = Path(__file__).parents[1] / "docs" / "site"
ALLOWED_STATUSES = {"baseline", "validated", "backlog"}
class SiteParser(HTMLParser):
def __init__(self) -> None:
super().__init__()
self.ids: set[str] = set()
self.references: list[str] = []
def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None:
values = dict(attrs)
if values.get("id"):
self.ids.add(values["id"])
attribute = "href" if tag in {"a", "link"} else "src" if tag in {"script", "img"} else None
if attribute and values.get(attribute):
self.references.append(values[attribute])
def _parser() -> SiteParser:
parser = SiteParser()
parser.feed((SITE_ROOT / "index.html").read_text())
return parser
def test_docs_site_required_files_are_present() -> None:
for relative_path in ("index.html", "styles.css", "app.js", "experiments.json", "og.png"):
path = SITE_ROOT / relative_path
assert path.is_file(), relative_path
assert path.stat().st_size > 0, relative_path
assert (SITE_ROOT / "og.png").read_bytes().startswith(b"\x89PNG\r\n\x1a\n")
def test_docs_site_local_links_and_anchors_resolve() -> None:
parser = _parser()
for reference in parser.references:
if reference.startswith("#"):
assert reference[1:] in parser.ids, reference
continue
parsed = urlparse(reference)
if parsed.scheme or parsed.netloc or reference.startswith("/"):
continue
if reference == "../static/favicon.svg":
continue
local_path = (SITE_ROOT / parsed.path).resolve()
assert local_path.is_relative_to(SITE_ROOT.resolve()), reference
assert local_path.is_file(), reference
def test_docs_site_navigation_targets_every_top_level_section() -> None:
parser = _parser()
html = (SITE_ROOT / "index.html").read_text()
expected_sections = {
"overview",
"architecture",
"agent-loop",
"workspace",
"models",
"experiments",
"operations",
"timeline",
"repository",
}
assert expected_sections <= parser.ids
for section_id in expected_sections:
assert f'href="#{section_id}"' in html
def test_experiment_ledger_has_unique_stable_records() -> None:
experiments = json.loads((SITE_ROOT / "experiments.json").read_text())
assert len(experiments) >= 5
ids = [item["id"] for item in experiments]
assert len(ids) == len(set(ids))
for item in experiments:
assert item["status"] in ALLOWED_STATUSES
assert item["title"].strip()
assert item["summary"].strip()
assert item["area"].strip()
assert item["version"].strip()
assert isinstance(item["metrics"], list)