Skip to content

Agents (project workspaces)

In-product Agents area: personal projects with an on-disk workspace ({OPENKMS_PROJECTS_ROOT}/{project_id}/), Deep Agents chat, file tree, local git, optional skills, and a global skills registry.

Area Status
Sidebar + /agents (Projects) + /agents/skills (Skills) + /projects/{id}/sessions/{sessionId} ✅
Project settings page /projects/{id}/settings (General includes git origin and delete project; Agent + Skills + Schedules + Sharing) ✅
Session API key (per conversation, creator identity) ✅
Global skills registry + project install ✅
Project CRUD + files API ✅
Conversations + NDJSON stream ✅
Plan mode toggle ✅
HITL resume endpoint ✅
Subagents (explore = workspace files; research = web + skills) ✅
Local git ✅
Remote git (HTTPS + PAT) ✅
Console Kubernetes cluster browse + apply/logs + register Service in Apps ✅
Project deploy secrets (encrypted → sync Opaque Secret; Agent sees names/keys only) ✅
Deploy via openkms skill (kubernetes apply / register-app / dev-sync) ✅ (API key needs console:kubernetes; register-app also ontology:write; dev-sync also projects:write)

Console operators can register clusters under /console/kubernetes (console:kubernetes); see Kubernetes clusters. Project settings → Deploy stores encrypted secrets and syncs them to the cluster; agents only reference Secret names. Workspace shell still runs in the backend/worker process. Operators can apply YAML in the cluster detail page; Agents use the openkms skill (kubernetes apply, kubernetes register-app, kubernetes dev-sync) — kubeconfig is never injected into the workspace. Dev-sync packs project files server-side into a Pod for hot iteration (see Dev sync).

Layout

  • Agents area: Projects | Skills tabs at /agents and /agents/skills. Project settings → General has a danger-zone Delete project (workspace files, sessions, and schedules).
  • Desktop (≥769px): three columns — left conversation sessions (like KB Q&A; a small running indicator when last_turn.status=running); center chat thread + collapsible Plan checklist (from write_todos, dismissible) + plan toggle + composer; right file tree + preview split with git actions. Drag pane dividers to resize; widths persist in localStorage. Opening Markdown (.md / .markdown) or HTML (.html / .htm) defaults to a rendered Preview (Markdown uses the same RichMarkdown + code/Mermaid pre component and .document-detail-markdown-body typography as document detail; [[target]] / [[target|label]] uses the wiki preprocess and opens another file in this project workspace, relative to the current file — bare names without an extension resolve to .md; HTML in a sandboxed iframe); toggle Source for the line-numbered raw view. Other text files stay Source-only. Assistant replies interleave text with compact tool and subagent rows (click a row to expand input/output); completed turns fold consecutive tool/subagent rows (≥2) into a single summary; the live turn stays expanded. History reloads tool rows from persisted transcripts. Message history is paged (tail first; scroll up loads older). Composer ignores Enter while an IME composition is active. Revert (under a user bubble): removes that message and all later turns, clears LangGraph checkpoint state for the session, and puts the user text back in the composer to edit and resend — refused while a non-stale turn is running. Upload menu: pick multiple files or a folder. Refresh, parent-folder navigation, and per-row delete (.openkms and .openkms/skills folders protected; other paths including files under .openkms/ may be deleted).
  • Shell: the app header is hidden (openkms-agents-fullpage), so the global left rail carries the brand → Home (top) and the account menu (bottom) — see Immersive views.
  • Phone (≤768px): chat is the primary full-height surface with a compact top bar (back, session title, Sessions / Files / Settings). Sessions and Files each open as a full-width panel (same idea as Documents channel list on phone). Opening a file in Files shows the viewer full-height (tree hidden until closed). Session Review uses Lessons | Improve | Artifacts tabs instead of three columns. App header stays hidden (openkms-agents-fullpage) — navigation is via the in-workspace chrome.

Session API key

Each project conversation (session) gets a dedicated personal API key (purpose=agent_session) owned by the session creator (user_sub). The plaintext token is stored encrypted in agent_conversations.context and injected into the agent shell as:

  • OPENKMS_API_KEY
  • OPENKMS_API_BASE_URL

Installed skills (e.g. openkms) use this key when run via shell execute. Keys are revoked when the session is deleted. They do not appear in Settings → API keys.

Run skill CLIs from the project root: python .openkms/skills/<skill_id>/scripts/cli.py … — no cd, no pip install (dependencies install with the skill).

Human-in-the-loop (HITL)

HITL is optional per tool name in hitl.py (currently none). The interrupt bar supports batched Approve all (N) when enabled. Resume streams into the current assistant turn.

When the agent pauses for approval, the turn is persisted as context.last_turn.status=interrupted with the interrupt payload in context.last_turn.interrupt (merged action_requests when multiple). Leaving the page or sleeping does not lose Approve/Reject: reopening the session restores the bar from last_turn. Resume / completion clears the stored interrupt.

Context compaction (long sessions)

Deep Agents compacts older turns when the model context budget is exceeded. Evicted history is offloaded to conversation_history/ in the project workspace; the agent keeps working from a structured internal summary. Compaction summaries are not shown in chat (they are filtered from the stream and stripped before persistence). In a single long turn with many tool calls, compaction may run more than once as context grows — that is normal and stays internal.

Before each workspace turn (interactive chat and scheduled runs), the backend may run the same pre-turn compaction step when the LangGraph checkpoint is over budget (deep_agents/turn_prepare.py). In-turn compaction inside Deep Agents still applies during long tool loops.

Checkpoint-first context (agent memory vs chat UI)

Layer Role
agent_messages (Postgres) What users see in the thread — user/assistant text and persisted tool trace pills for the UI
LangGraph checkpoint (thread_id = conversation id) What the agent uses on the next turn — full tool messages, subagent state, compaction events

On revert, checkpoint rows are deleted with the truncated messages so the next turn re-seeds from DB. On normal follow-up turns, only the latest user message is appended to the checkpoint thread (tool outputs are not re-injected from agent_messages, avoiding duplicate tokens). HITL resume continues from the interrupted checkpoint via Command(resume=…) without re-sending DB history.

LangGraph checkpoints use Postgres tables checkpoints, checkpoint_blobs, checkpoint_writes with thread_id = conversation id. The backend checkpointer uses a connection pool so concurrent turns (and revert-then-resend) do not share one psycopg connection. Revert deletes checkpoint rows on the same SQLAlchemy transaction as message deletes (DELETE …/messages/from/{id}).

Image input (multimodal turns)

Attach images in the composer — paste, drag & drop, or the paperclip button (PNG / JPEG / WebP / GIF, max 8 MB). Chips preview above the input; send with or without text.

  • Storage: object storage under agent-attachments/{conversation_id}/ — deliberately not the project workspace, so git status, remotes, and the agent's file tools never see chat images. Uploads go to POST …/conversations/{id}/attachments.
  • Persistence: agent_messages.attachments holds descriptors only (id, name, mime, size); the image bytes stay in object storage and are inlined as the turn is built.
  • Turn input: deep_agents/turn_input.py builds HumanMessage content blocks (text + image_url with a base64 data URI) for both the checkpoint-first append and the DB seed path, so replayed and reverted threads keep their images. Images then live in the checkpoint and are re-sent each turn until compaction offloads them (_aoffload_inline_media).
  • Cleanup: removing a staged chip deletes its object; posting a message sweeps older unreferenced uploads; deleting a conversation or reverting/truncating messages removes the matching objects. Deleting a conversation purges its whole prefix.
  • Model support: needs an image-capable model (deepseek-flash, vision capability in Models). Deep Agents replaces content blocks the active model profile rejects, so a text-only model degrades instead of failing.
  • Discard: DELETE …/conversations/{id}/attachments/{attachment_id}?mime=… removes a staged image (refused with 409 once a message references it).

Skills

Layer Location
Registry (global) {OPENKMS_AGENT_SKILLS_ROOT}/{skill_id}/{version}/ + DB tables agent_skills, agent_skill_versions
Installed (per project) {project_id}/.openkms/skills/{skill_id}/ + projects.settings.installed_skills
  • Upload zip or folder on Agents → Skills (POST /api/agent-skills). Each version stores a content hash (per-file SHA-256, sorted, aggregated).
  • Skill settings (/agents/skills/{skill_id}/settings): general metadata and version upload/delete (same layout as channel settings).
  • Set default version per skill (used when installing without picking a version).
  • Toggle install on new projects in skill settings (is_default); matching skills auto-install when creating a project.
  • Project settings → Skills: install/update/uninstall skills from the registry.

Scheduled agent runs

  • Project settings → Schedules: create cron jobs that send a fixed prompt to the project agent.
  • Stateless: new chat session each run; optional delete session after completion.
  • Stateful: reuses one existing session (conversation_id); keeps context between runs.
  • Not supported: plan mode.
  • Unattended: HITL is disabled; tool approvals are auto-approved. Context is compacted before each run when over the model budget (same pre-turn step as interactive chat; see Context compaction above).
  • Auth uses a session API key minted for the schedule owner (owner_sub + optional realm roles in config).
  • All rows appear in Job runs → Schedules (/job-runs/schedules); worker task run_scheduled_project_agent.

Configuration

Variable Default Notes
OPENKMS_PROJECTS_ROOT data/projects (local) / /data/projects (Docker) One folder per project UUID
OPENKMS_AGENT_SKILLS_ROOT data/agent-skills Global skills registry
OPENKMS_DEEP_AGENT_MODEL_ID — Falls back to OPENKMS_AGENT_MODEL_ID
OPENKMS_AGENT_LLM_EXTRA_BODY unset Deep Agents façade (deep_agents/llm_chat.py); enable_thinking always forced false
OPENKMS_AGENT_LLM_REASONING_CONTENT_SHIM unset (auto) Same façade: inject empty reasoning_content on assistant rows for non-api.openai.com bases
OPENKMS_AGENT_SANDBOX_TIMEOUT_SECONDS 60 Python sandbox in project dir
OPENKMS_AGENT_LOG_LEVEL INFO Project agent turn logs (agent_turn_start / agent_turn_done at INFO; agent_turn_failed at ERROR). Use DEBUG for verbose deep-agents detail.
OPENKMS_BACKEND_LOG_LEVEL — Root log level for all app.* loggers when set (default INFO).

Observability (without Langfuse)

Interactive agent turns run in the backend container; scheduled cron runs run in the worker container. Tail both:

docker compose -f docker/docker-compose.yml logs -f backend worker

Filter failed turns (visible at default OPENKMS_AGENT_LOG_LEVEL=INFO):

docker compose -f docker/docker-compose.yml logs -f backend 2>&1 | rg 'ERROR.*agent_turn'

Each turn logs grep-friendly fields: turn_id, project_id, conversation_id, duration_ms, and on failure error=…. Scheduled run failures also appear in Job runs → detail (worker_log).

The latest turn summary is stored on the conversation as context.last_turn (turn_id, status, error, duration_ms, tool_count, optional assistant_message_id, optional interrupt when status=interrupted) — visible via GET …/conversations/{id} for debugging. Interactive stream turns commit the user message and an empty assistant row before the agent loop (status=running), then throttle-update the assistant row (about every 2s and on tool events) while the model runs. Persisted tool_calls include wiki_tool_traces_v1 (model replay) and wiki_assistant_stream_parts_v1 (interleaved text + tool order for the chat UI after reload). Older assistant rows that only have traces (no stream parts) cannot recover mid-turn order; the SPA falls back to text then folded tools. The agent loop itself runs in a background task detached from the browser connection: leaving the page or the machine sleeping cancels only the NDJSON stream, not the turn. Reopening the session polls while last_turn.status is running (and not stale — same ~2h window as the API / STALE_RUNNING_SECONDS) and reloads messages until the turn leaves running. Switching to another session aborts the browser’s NDJSON reader (frees the composer on the other session) while the server turn continues; returning to the session attaches via poll / mid-turn DB rows. Failed stream turns persist an assistant message in the thread and emit an NDJSON error line (detail + persisted message) when the client is still connected. GET …/messages?tail=true returns the last page in one request. Deleting a session clears the LangGraph checkpoint thread and is refused while a non-stale turn is running.

Langfuse (optional): Same variables as qa-agent: LANGFUSE_SECRET_KEY, LANGFUSE_PUBLIC_KEY, and LANGFUSE_BASE_URL must all be set or tracing is off. Pass optional session_id on POST …/messages to group turns in one Langfuse Session (defaults to conversation id). Tags: deep-agent, project-stream | project-sync, and plan-mode when applicable. LANGFUSE_HEALTHCHECK (default true) probes the host before callbacks; LANGFUSE_TRACE_STREAMING (default true) controls streaming turns.

Project search: In Agent settings, enable web search and pick a search_tool connector. Stored as web_search and search_connector_id in projects.settings.

Docker: projects_data volume on backend and worker (include agent-skills under the same volume or a sibling mount).

Troubleshooting

Symptom Likely cause What to check
Toast network error / fetch failed after ~5 min pending Proxy read timeout while the agent is still running but not streaming bytes (long tool/LLM gap). Stack: browser → host nginx → Docker frontend nginx (proxy_read_timeout 300s in docker/nginx-frontend.conf) → backend. Turn usually keeps running server-side; reopen the session to see progress / final reply. Raise proxy_read_timeout (and proxy_send_timeout) on both nginx layers for location /api/; tail host /var/log/nginx/error.log for upstream timed out; check context.last_turn.
Empty chat after leave/sleep (pre-fix) Stream was bound to the HTTP request: disconnect cancelled the turn and rolled back the uncommitted messages. Fixed: early commit + background turn + poll on reopen. Redeploy backend/frontend if still seen on an old build.
Mid-turn UI frozen after fast session switch Older SPA builds could drop the live NDJSON bridge without re-attaching poll. Current SPA aborts NDJSON on leave, marks running optimistically, and polls when the session is visible again. Hard-refresh if on an old bundle.
409 A turn is already running Another interactive turn for the session has last_turn.status=running (or a stale one under ~2h). Wait / reopen session; if the API process died mid-turn, wait for stale timeout or inspect last_turn.
agent_turn_failed in backend logs with LLM/tool text; error line in chat after refresh In-app failure (misconfigured model, recursion limit, tool error). HTTP 200 + NDJSON error. docker compose logs -f backend 2>&1 \| rg 'ERROR.*agent_turn'; conversation context.last_turn.
400 reasoning_content … must be passed back (DeepSeek thinking) Thinking mode on + tools; history missing reasoning_content. Deep Agents disables thinking + shims via deep_agents/llm_chat.py. Redeploy backend; or set OPENKMS_AGENT_LLM_REASONING_CONTENT_SHIM=true.
another command is already in progress (psycopg) on revert + resend Fixed: checkpointer now uses AsyncConnectionPool. Redeploy backend if still seen on an old image. Ensure no overlapping streams on the same session (wait for revert toast before resend).
Empty chat after reload, toast on load GET …/messages failed (auth/network). Browser network tab; backend access log.

Local dev (./dev.sh, Vite 5173) has no nginx; long-turn timeouts are uncommon unless a host reverse proxy sits in front.

Runtime (Deep Agents)

Workspace chat uses deepagents ≥0.7.21. The Plan checklist UI depends on write_todos, so the workspace factory opts in with LangChain TodoListMiddleware (opt-in since deepagents 0.7). Plan mode is hard read-only by replacing FilesystemMiddleware with a tools allowlist (ls / read_file / glob / grep only) — deepagents 0.7 does not support FilesystemPermission together with a shell-capable backend. Explore always uses that read-only filesystem stack; research uses it in plan mode.

Implementation layout

Module Role
deep_agents/factory.py Build workspace and improvement Deep Agents
deep_agents/turn_prepare.py Pre-turn compaction + checkpoint-first message input
deep_agents/llm_chat.py ChatOpenAI + thinking / reasoning_content shim
agent/tool_transcripts.py Persisted tool traces for UI replay (wiki_tool_traces_v1)
deep_agents/constants.py STALE_RUNNING_SECONDS (sync with frontend)

Remaining gaps: Tech debt — Project agents.

Project folder

{project_id}/
  AGENTS.md
  .gitignore           # ignores .openkms/skills/, conversation_history/, large_tool_results/
  .openkms/skills/     # installed skills (e.g. openkms/)
  .git/                # optional

Runtime settings (web search, git identity, installed_skills) live in projects.settings (PostgreSQL).

The project agent system prompt includes the project name, slug, description, workspace scope (paths relative to project root), installed skills under .openkms/skills/, and the contents of AGENTS.md. SkillsMiddleware loads SKILL.md from each installed skill (parent source path .openkms/skills/).

Permissions

  • projects:read — list projects/skills, read files, chat read
  • projects:write — create/update/delete projects, upload skills, agent messages, git, install skills

Feature toggle: agents (Console → Feature toggles).

Git

  • Local: init, status, log, add, commit via the files rail. The git branch button opens a Source Control view modelled on VS Code: branch pill with ahead/behind and pull/push, a commit message box at the top (primary Commit for what is staged, with a Commit All / Commit & Push dropdown), Staged Changes and Changes groups with per-file stage / unstage / discard, a unified diff in the viewer pane (untracked files show as whole-file additions), and a commit list with ref badges. Toggle back to the file tree with the same button or All files. Agent shell (execute) can still run git in the project folder.
  • Remote: HTTPS + PAT only; credentials in Profile → Git credentials. On create project, optional repository URL, branch, and credential clone into the workspace. Project settings → General shows the live origin URL and current branch, lets you change origin (GitHub → GitLab and similar), and pull/push with a saved credential. Clone / pull / push APIs on /api/projects/{id}/git/*. Saving origin initializes git if needed and stores settings.git.remote_url (and optional credential_id).

See API reference — Projects and API reference — Agent skills.