Spring AI LoomAgent API Documentation

Base URL: http://localhost:8080 (default port for the test environment) Version: 1.2.0 Authentication: The project uses a BFF (Backend-For-Frontend) + HttpOnly Cookie auth model. After login, the server sets a loom-agent-session cookie via Set-Cookie header. The browser automatically includes this cookie in subsequent requests. No token storage or manual header management is required.


Table of Contents


1. User Authentication

1.1 Check Auto-Login Status

POST /spring/ai/loom/user/isAutoLogin

Request Body: None

Response: boolean

Value Description
true Auto-login is enabled
false Auto-login is not enabled

1.2 User Login

POST /spring/ai/loom/user/login

Request Body:

Field Type Required Description
username string Yes Username
verified string No Verification info

Example:

{
 "username": "testuser",
 "verified": ""
}

Response (UserResponseRecord):

Field Type Description
token string Session token (also set as HttpOnly cookie)
nickname string User nickname

Response Headers:

  • Set-Cookie: loom-agent-session=<token>; Max-Age=86400; Path=/; HttpOnly; SameSite=Lax

Example:

{
 "token": "567fb50c-b293-403b-a903-f6b7b597c318",
 "nickname": "用户"
}

1.3 User Logout

POST /spring/ai/loom/user/logout

Request Body: None

Response: true (boolean)

Effect: The session token is invalidated server-side and the loom-agent-session cookie is cleared (Max-Age=0).


2. Conversation Management

2.1 List Conversations

GET /spring/ai/loom/conversation

Headers: Must include authentication info.

Response: ConversationRecord[]

Field Type Description
conversationId string Conversation ID
title string Conversation title

Example:

[
 {
 "conversationId": "conv-001",
 "title": "First Conversation"
 }
]

2.2 Get Conversation History

GET /spring/ai/loom/conversation/{conversationId}

Path Parameters:

Parameter Type Description
conversationId string Conversation ID

Response: Object[] — Spring AI chat memory messages plus injected tool-call entries.

The array contains two shapes:

(a) Chat memory messages (unchanged shape — USER / ASSISTANT):

Field Type Description
messageType string USER or ASSISTANT
text string Message content
metadata object For ASSISTANT, may carry thinking (per-turn reasoning, injected by ChatReasoningHistory.attachThinking)
media array Attached media (usually empty)

(b) Tool-call entries (messageType = "TOOL_CALL", injected by ToolCallHistory from loom_tool_call_log):

Field Type Description
messageType string Always TOOL_CALL
toolName string e.g. askUser, startSubTask, getCurrentTime
toolCallId string Spring AI tool-call id
arguments object Parsed tool arguments. Parse failure degrades to {"_raw": "...", "_parseError": true}
result string Tool return value, truncated to 2 KB
isError boolean Whether the tool returned an error
durationMs number Tool execution time
truncated boolean true when result was truncated
createdAt string ISO-8601 timestamp

Why tool calls are injected separately: SPRING_AI_CHAT_MEMORY has no metadata column (only conversation_id / content / type / timestamp / sequence_id), so tool invocations are not stored there. Without injection, historical replay would lose the askUser cards and sub-task chips entirely.

Truncation thresholds (two layers, deliberately distinct): the whole arguments JSON triggers truncation above 1024 chars, then each non-exempt field is capped at 256 chars; result is capped at 2048 chars. askUser.optionsJson is exempt — it is the sole source of the card's options.

Cross-user read: returns 403 with an empty array (same shape as success, so client code does not break).


2.3 Delete Conversation

DELETE /spring/ai/loom/conversation/{conversationId}

Path Parameters:

Parameter Type Description
conversationId string Conversation ID

Response: true (boolean)


3. Streaming Chat

3.1 SSE Streaming Chat

POST /spring/ai/loom/stream
Content-Type: application/json
Accept: text/event-stream

Request Body (ChatRequestRecord):

Field Type Required Description
message string Yes User message content
conversationId string No Conversation ID. A new session is created if omitted.
mcps string[] No List of MCP tool names to enable
enabledKnowledgeIds string[] No Knowledge base IDs for RAG retrieval (multiple)
fileIds string[] No List of associated file IDs (supports multiple files)
selectedSkillName string No Skill name picked via the / picker; injected as a strong directive into the system prompt, bypassing LLM tool-selection bias
enabledToolGroups string[] No Tool groups the user ticked in the panel. null/empty falls back to the role-granted full set; non-empty is intersected with role grants

Example:

{
 "message": "Please summarize this document",
 "conversationId": "conv-001",
 "knowledgeId": "kb-001",
 "mcps": [],
 "fileIds": ["file-abc123", "file-def456"]
}

Response: SSE (Server-Sent Events) stream.

Each event returns a ChatResponseRecord:

Field Type Description
content string AI response text fragment
reasoningContent string Reasoning/thinking trace (optional)
askUser object | null 3rd component. null = normal content frame; non-null = an askUser question-card event (AskUserEvent, fields below). The frontend renders an inline choice card and the askUser tool blocks until answered
subTaskEvent object | null 4th component. null = normal frame; non-null = a sub-task lifecycle event (SubTaskEvent) driving the compact chip shown live in the chat stream

askUser (AskUserEvent) fields:

Field Type Description
questionId string UUID; index for the answer endpoint
question string Question body
header string Short title/chip (nullable)
background string Background context (nullable)
options object[] 2-4 options, each {label, description}
multiSelect boolean Whether multiple options can be selected
allowCustomInput boolean Whether a custom "other" input is allowed
timeoutSeconds number Frontend countdown; same value as the tool's blocking timeout

SSE Event Example:

data: {"content":"Hello","reasoningContent":""}

data: {"content":"!","reasoningContent":""}

data: {"content":"How can","reasoningContent":""}

data: {"content":"I help you?","reasoningContent":""}

3.2 AskUser answer endpoint

POST /spring/ai/loom/ask/{questionId}/answer
Content-Type: application/json

Submits the answer to a question card (the askUser tool is blocking and waiting).

  • Request body: {"answer": "option label"} (single-select / custom input) or {"answer": ["label1","label2"]} (multi-select)
  • 200 {"ok":true} — answer delivered, the blocked tool thread is woken up
  • 400 {"error":"invalid answer"} — malformed body / answer missing or blank
  • 404 {"error":"not found"} — unknown questionId, cross-user submission, or the question already timed out / was cancelled (all three share one response to prevent existence leaks)

4. File Management

4.1 Upload File

POST /spring/ai/loom/file/upload
Content-Type: multipart/form-data

Form Fields:

Field Type Required Description
file file Yes File to upload

Response:

Field Type Description
fileId string File ID returned after upload
status string Upload status, "success" on success

Example:

{
 "fileId": "file-abc123",
 "status": "success"
}

4.2 List File Tree

GET /spring/ai/loom/file
GET /spring/ai/loom/file/tree

Response: Directory tree JSON (recursive structure).

Field Type Description
name string File or directory name
type string "file" or "directory"
size number File size in bytes (files only)
children array Child items (directories only)

Example:

{
 "name": ".",
 "type": "directory",
 "children": [
 {
 "name": "report.pdf",
 "type": "file",
 "size": 102400
 },
 {
 "name": "docs",
 "type": "directory",
 "children": [
 { "name": "guide.md", "type": "file", "size": 4096 }
 ]
 }
 ]
}

4.3 View File by Path

GET /spring/ai/loom/file/by-path/view?path=report.pdf

Query Parameters:

Parameter Type Required Description
path string Yes File path relative to user's file directory

Response: 307 Temporary Redirect → /file/view/{fileId} (WOPI file viewer).

If the file is not yet registered in file_info, a temporary record (usage="temp") is auto-created to obtain a fileId.


4.4 Download File by Path

GET /spring/ai/loom/file/by-path/download?path=report.pdf

Query Parameters:

Parameter Type Required Description
path string Yes File path relative to user's file directory

Response: 307 Temporary Redirect → /wopi/files/{fileId}/contents.


4.5 Download File by ID

GET /spring/ai/loom/file/{id}/download

Path Parameters:

Parameter Type Description
id string File ID

Response: File binary stream, Content-Disposition header includes original filename.


5. Knowledge Base Management

5.0 Feature Flags

GET /spring/ai/loom/api/features

Response: { "knowledge": boolean } — knowledge is true iff the container has a VectorStore bean (RAG chain active). When spring.ai.loom.agent.rag.enabled=false (or no EmbeddingModel bean exists), it is false and the frontend hides the knowledge-space (📚) button.

5.1 Check Knowledge Upload Status

GET /spring/ai/loom/knowledge/checkKnowledgeUpload

Response: boolean — Whether knowledge upload functionality is available (false when RAG is globally disabled).


5.2 List Knowledge Bases

GET /spring/ai/loom/knowledge

Response: KnowledgeRecord[]

Field Type Description
id string Knowledge base ID
username string Creator username
name string Knowledge base name

Example:

[
 {
 "id": "kb-001",
 "username": "admin",
 "name": "Product Docs"
 }
]

5.3 Create Knowledge Base

PUT /spring/ai/loom/knowledge
Content-Type: application/json

Request Body (KnowledgeRecord):

Field Type Required Description
name string Yes Knowledge base name

Example:

{
 "name": "Product Docs"
}

Response: The created KnowledgeRecord (including the generated id).


5.4 Delete Knowledge Base

DELETE /spring/ai/loom/knowledge/{knowledgeId}

Path Parameters:

Parameter Type Description
knowledgeId string Knowledge base ID

Deleting a knowledge base cascades to clean up associated files and vector data.

Response: number — Number of deleted records.


5.5 Upload File to Knowledge Base

POST /spring/ai/loom/knowledge/{knowledgeId}/upload
Content-Type: multipart/form-data

Path Parameters:

Parameter Type Description
knowledgeId string Knowledge base ID

Form Fields:

Field Type Required Description
file file Yes File to upload

Response:

{
 "fileId": "file-xyz789",
 "status": "success"
}

5.6 List Files in Knowledge Base

GET /spring/ai/loom/knowledge/{knowledgeId}/file

Path Parameters:

Parameter Type Description
knowledgeId string Knowledge base ID

Response: FileRecord[] (same format as 4.2).


5.7 Delete File from Knowledge Base

DELETE /spring/ai/loom/knowledge/{knowledgeId}/file/{fileId}

Path Parameters:

Parameter Type Description
knowledgeId string Knowledge base ID
fileId string File ID

Response: number — Number of deleted records.


5.8 Knowledge Market

Knowledge market enables sharing knowledge bases across users. Approval flow: submit → PENDING → admin approve/reject (POST /admin/market-knowledge/{id}/approve|reject); only APPROVED entries are publicly listed and pullable. Re-submitting a REJECTED entry archives the old row (id-preserving, incl. reject comment/reviewer/time) into loom_market_knowledge_archive and creates a NEW PENDING row.

5.8.1 Browse Approved Market Knowledge Bases

Kept v1 leg (roles.js dependency): GET /api/knowledge-market still returns the APPROVED-only list; the richer v2 list (GET /market-knowledge, paged with tag/category filters) is the primary browse endpoint for the frontend.

GET /spring/ai/loom/api/knowledge-market?page=1&size=20

Query Parameters:

Parameter Type Required Description
page int No Page number (default 1)
size int No Items per page (default 20)

Response: MarketKnowledgeRecord[] — Approved market knowledge bases.

Field Type Description
id string Market knowledge ID
username string Original author username
name string Knowledge base name
description string Knowledge base description
status string APPROVED
submittedAt string Submission timestamp
reviewedAt string Review timestamp

5.8.2 Subscribe to Market Knowledge Base

POST /spring/ai/loom/api/knowledge-market/{marketId}/pull

Path Parameters:

Parameter Type Description
marketId string Market knowledge ID

Response: {"success": true} on success. Creates a subscription in loom_user_knowledge with source=MARKET_PULLED. Requires the entry to be APPROVED — non-APPROVED returns 403.


5.8.3 Submit Knowledge Base to Market

POST /spring/ai/loom/api/knowledge/{knowledgeId}/submit

Path Parameters:

Parameter Type Description
knowledgeId string Knowledge base ID

Response: MarketKnowledgeRecord — The created market entry with status='PENDING' (awaits admin review).

Behavior: If a same-(username, name) entry exists → depends on its status: REJECTED → old row archived to loom_market_knowledge_archive (id-preserving, includes reject comment/reviewer/time), a NEW PENDING row is inserted (new id). PENDING / APPROVED → in-place content update, status untouched (APPROVED never demotes). Otherwise → INSERT a new PENDING row (created_by_kind='USER').


5.8.4 Withdraw Market Submission

DELETE /spring/ai/loom/api/knowledge-market/{marketId}

Path Parameters:

Parameter Type Description
marketId string Market knowledge ID

Response: {"success": true} on success.

Behavior: Unified DELETE endpoint for both author withdraw and admin takedown. Internal permission check decides path:

  • Author: DELETE FROM loom_market_knowledge WHERE id=? AND username=? (only deletes own)
  • Admin: DELETE FROM loom_market_knowledge WHERE id=? (any row)

Cascading cleanup (both paths): Auto-deletes loom_user_knowledge (subscriber rows) + loom_role_knowledge (role grants) referencing the market entry.

Approval endpoints (v2 admin router — the v1 /api/knowledge-market/{marketId}/approve|reject paths remain retired; approve/reject now live under /admin):

  • POST /admin/market-knowledge/{id}/approve — admin approve: status=APPROVED, reviewed_at/reviewed_by set
  • POST /admin/market-knowledge/{id}/reject — admin reject: status=REJECTED; body {"comment": "..."} is required (empty/missing → 400 both client-side and server-side)

Admin endpoints:

  • GET /admin/market-knowledge — list all market knowledge bases (all statuses: PENDING / APPROVED / REJECTED)
  • POST /admin/market-knowledge — admin create: direct APPROVED + created_by_kind='ADMIN'. Body: MarketCreateRequest (name / description / content / category)
  • PUT /admin/market-knowledge/{id} — edit fields (MarketUpdateRequest) — cannot change status (no status field); state changes only via approve/reject
  • DELETE /admin/market-knowledge/{marketId} — admin takedown (cascade cleanup of loom_user_knowledge + loom_role_knowledge reference rows)
  • Plus per-id admin legs mirroring the skill market: /official, /featured-rank, /category, /announcement (PUT + DELETE), /reviews/{username} (DELETE), /stats-reset, /tags (PUT + GET)

5.8.5 Admin Approve / Reject Market Submission

POST /spring/ai/loom/admin/market-knowledge/{id}/approve
POST /spring/ai/loom/admin/market-knowledge/{id}/reject

Admin-only (non-admin → 403). approve sets status=APPROVED and records reviewed_at / reviewed_by. reject sets status=REJECTED and requires a non-blank comment in the body ({"comment": "reason"}) — missing/blank → 400. The author sees the reject reason in 我的发布 and can re-submit: the REJECTED row is archived to loom_market_knowledge_archive (id-preserving, includes comment/reviewer/time) and a fresh PENDING row is created.


5.8.6 Admin Reject Market Submission (legacy v1 path)

POST /spring/ai/loom/api/knowledge-market/{marketId}/reject

removed — the v1 KB approve/reject legs are retired; use the v2 admin endpoints POST /admin/market-knowledge/{id}/approve|reject (see § 5.8.5) instead.


5.8.7 List My Subscribed Knowledge Bases

GET /spring/ai/loom/api/knowledge-market/my-pulled

Response: MarketKnowledgeRecord[] — Knowledge bases the current user has subscribed to from the market.


5.8.8 List My Market Submissions

GET /spring/ai/loom/api/knowledge-market/my-submitted

Response: MarketKnowledgeRecord[] — Knowledge bases the current user has submitted to the market (PENDING / APPROVED / REJECTED).


6. Skill Management

All skills live in the database (tables market_skill / user_skill / role_skill). The yml skills[] block is no longer read — it was replaced by 6 system-seeded user_skill rows plus an admin-controlled market workflow.

Three sources show up in the source field of user_skill:

  • USER_CREATED — created by the user via API or chat UI; fully editable (name/desc/content/default_loaded)
  • MARKET_PULLED — pulled from the approved market; default_loaded toggle only (name / content / desc locked to the market snapshot; re-pull to update, or use POST /skill/{name}/duplicate to copy as a new USER_CREATED skill and edit that)
  • ROLE_GRANTED — auto-injected from a role authorization; read-only (locked=true; cannot edit or delete)

Every list/get call auto-syncs role_skill → user_skill for the current user, so a freshly authorized skill is visible immediately on next list.


6.1 List Current User's Skills

GET /spring/ai/loom/skill

Response: SkillRecord[] (the LLM-facing view)

Field Type Description
name string Skill name
description string Skill description
load boolean Whether the LLM preloads this skill into the system prompt
content string Skill content (resolved: if stored as classpath:xxx, the resource is read at this point)
source string USER_CREATED / MARKET_PULLED / ROLE_GRANTED (admin no longer gets a MARKET_VIEW union; the market Tab is the only place to browse APPROVED market entries)

For admins, this list shows only their own user_skill entries (the same view as for any other user). Browse the approved market via the GET /market-skills endpoint.

6.2 Create / Overwrite a Skill (USER_CREATED)

PUT /spring/ai/loom/skill
Content-Type: application/json

Request Body (SkillRecord):

Field Type Required Description
name string Yes Skill name
description string No Skill description
load boolean No Whether the LLM preloads this skill; defaults to true
content string Yes Skill content / prompt template (supports classpath: prefix which is resolved on read)

{param} placeholders inside content are LLM-interpreted at runtime, not declared as structured form fields. MCP tool references inside content use @tool_name and resolve to the role-authorized MCPs.

Example:

{
 "name": "email_writer",
 "description": "Professional email writing assistant",
 "load": true,
 "content": "You are an email assistant. The recipient is {recipient}..."
}

Response: true (boolean) — false if a ROLE_GRANTED skill with the same name is locked.


6.3 Patch Description / Default-Loaded

PATCH /spring/ai/loom/skill/{name}
Content-Type: application/json

For USER_CREATED skills — change description and/or default_loaded without overwriting content. For MARKET_PULLED skills — only default_loaded may change; a description change is rejected with 403 (locked to the market snapshot). Returns 400 if the skill is ROLE_GRANTED (locked).

Request Body (UserSkillPatchRequest):

Field Type Description
description string New description (omit to leave unchanged)
defaultLoaded boolean New default-loaded flag (omit to leave unchanged)

6.4 Get a Single Skill (LLM-facing)

GET /spring/ai/loom/skill/{name}

Path Parameters:

Parameter Type Description
name string Skill name

Response: SkillRecord. Admins see only their own user_skill (no market-view fallback — consistent with §6.2).


6.5 Delete a Skill

DELETE /spring/ai/loom/skill/{name}

Returns 400 if the skill is ROLE_GRANTED (locked). Returns true on success.


6.6 Trigger Role Sync Manually

POST /spring/ai/loom/skill/sync

Re-runs the role_skill → user_skill sync for the current user. Mostly for debugging — the same sync runs automatically on every list/get.


6.7 Skill Market — Browse (any user)

GET /spring/ai/loom/market-skills

Returns all market_skill rows with status='APPROVED', ordered by author, name (the version field has been removed — no version DESC ordering any more). Each item has the full MarketSkill model (id, name, description, content, author, status, submittedAt, reviewedAt, reviewedBy, reviewComment).


6.8 Skill Market — View One

GET /spring/ai/loom/market-skills/{id}

Path Parameters: id = market_skill.id (Long)


6.9 Skill Market — Pull into My user_skill

POST /spring/ai/loom/market-skills/{id}/pull

Creates / updates a MARKET_PULLED user_skill row from the given market_skill. Throws 403 if the market skill isn't APPROVED (approval flow — only approved entries are pullable). Throws 400/403 if:

  • A ROLE_GRANTED lock with the same name already exists
  • A same-name USER_CREATED skill exists in your user_skill (403 — pull refuses to overwrite self-built content; delete it first or use duplicate)

A same-name MARKET_PULLED row is refreshed in place (content updated to the latest market snapshot, no error).


6.10 Submit My Skill to Market

POST /spring/ai/loom/user/market-skills
Content-Type: application/json

Submits a new market_skill row with status=PENDING and author=currentUser (approval flow — awaits admin approve/reject; created_by_kind='USER').

Request Body (MarketSkillSubmitRequest):

Field Type Required Description
name string Yes Skill name
description string No Description
content string Yes Prompt template

The version field has been removed — the (author, name) pair is the unique constraint.

Behavior: If (author, name) already exists in market_skill, the outcome depends on its status:

  • REJECTED → the old row is archived to market_skill_archive (id-preserving, includes reject comment/reviewer/time), and a NEW PENDING row is inserted (new id). The author's user_skill.market_skill_id backlink is rebound to the new row.
  • PENDING / APPROVED → in-place content/desc update, status untouched (APPROVED never demotes back to PENDING).

Otherwise INSERT a new row with status=PENDING. The author's user_skill.market_skill_id is bound to the market_skill row so that subsequent save propagates to all pullers.


6.11 Admin — Market CRUD (any status)

Admin-only. Auth-gated by auth.adminPathPatterns and double-checked in the handler.

Method Path Description
GET /spring/ai/loom/admin/market-skills List all statuses (PENDING / APPROVED / REJECTED); optional ?status=PENDING filter (there is no separate /pending endpoint), plus page / size / category / query / sortBy
POST /spring/ai/loom/admin/market-skills Admin create — direct status=APPROVED + created_by_kind='ADMIN' (bypasses PENDING). Body: MarketCreateRequest (name / description / content / category)
PUT /spring/ai/loom/admin/market-skills/{id} Edit any field of any market skill. Body: MarketUpdateRequest — cannot change status (no status field); state changes only via approve/reject
DELETE /spring/ai/loom/admin/market-skills/{id} Cascade-deletes from user_skill and role_skill(这就是"下架",拉取者失去该 skill)
POST /admin/market-skills/{id}/approve Admin approve: status=APPROVED, reviewed_at / reviewed_by set
POST /admin/market-skills/{id}/reject Admin reject: status=REJECTED; body {"comment": "..."} required (empty/missing → 400). Author re-submit of a REJECTED row archives it to market_skill_archive and creates a NEW PENDING row
never existed /admin/market-skills/pending No separate endpoint — use GET /admin/market-skills?status=PENDING
PUT /admin/market-skills/{id}/official | /featured-rank | /category | /announcement | /stats-reset | /tags Per-row admin legs (official flag / featured rank / category / announcement / stats reset / tag management); plus GET .../tags, DELETE .../announcement, DELETE .../reviews/{username}

Request bodies:

MarketCreateRequest (POST — admin create):

Field Type Required Description
name string Yes Skill name
description string No Description
content string Yes Prompt template
category string No Category

MarketUpdateRequest (PUT — admin edit; no status field — status changes only via approve/reject):

Field Type Required Description
name string No Rename; null = no change
description string No Description
content string No Prompt template
category string No Category
isOfficial boolean No Official flag
featuredRank int No Featured ranking

MarketSkillUpsertRequest (+ ISkillMarketService.adminCreate/adminUpdate/adminDelete) are @Deprecated (ADR-T03 shim, kept 1 minor version) — the v2 createApproved / update / delete paths above are the supported entry points.


6.12 Admin — Authorize a Skill to a Role

GET /spring/ai/loom/admin/roles/{code}/skills
PUT /spring/ai/loom/admin/roles/{code}/skills

GET returns the role's currently authorized market_skill list as RoleSkillItem[] (each item has marketSkillId + defaultLoaded).

PUT replaces the whole list. Request body:

{
 "items": [
 {"marketSkillId": 1, "defaultLoaded": true},
 {"marketSkillId": 5, "defaultLoaded": false}
 ]
}

defaultLoaded defaults to true if omitted. The role's users will see these skills injected into their user_skill (source=ROLE_GRANTED, locked=true) on their next list/sync.


7. MCP Tools

7.1 List MCP Servers and Tools

GET /spring/ai/chat/loom/mcp

Response: McpRecord[]

Field Type Description
name string MCP server name
title string MCP server title (display label)
version string MCP version
description string MCP description
defaultSelected boolean Whether selected by default
tools ToolRecord[] Tool list

ToolRecord:

Field Type Description
name string Tool name
description string Tool description

Example:

[
 {
 "name": "weather-mcp",
 "title": "Weather",
 "version": "1.2.0",
 "description": "Provides real-time weather query service",
 "defaultSelected": true,
 "tools": [
 {
 "name": "getWeather",
 "description": "Query current weather for a specified city"
 }
 ]
 }
]

8. Skill Tools (LLM-callable)

The @Tool methods registered by ISkillTool for the LLM. These are separate from the admin REST APIs in ## 6. Skill Management (which serve the admin console UI).

The full skill catalog (name + description) is auto-injected into the system prompt under the 技能 section. The LLM does not need a list tool to discover skills — it calls @getSkill directly to fetch full content. This mirrors how IKnowledgeTool has no listKnowledgeBases tool.

8.1 getSkill — fetch one skill's content

@Tool: getSkill
Parameter Type Required Description
name string Yes The skill's name

Returns: Text with 技能名, 技能描述, and full 技能内容. Throws if the user has no access to the skill.

8.2 createOrUpdateSkill — create or update a user-owned skill

@Tool: createOrUpdateSkill
Parameter Type Required Description
name string Yes Skill name; trimmed; max length 128
description string No Skill description
content string Yes Skill prompt content (non-empty)

Behavior:

  • If the user already owns a skill with this name, content/description are overwritten.
  • ROLE_GRANTED / MARKET_PULLED skills are locked and will return 403.
  • Returns "已创建技能 X" or "已更新技能 X".

9. Knowledge Tools (LLM-callable)

The @Tool methods registered by IKnowledgeTool for tool-based RAG retrieval. The list of enabled knowledge bases is auto-injected into the system prompt under the 【知识库】 section — the LLM does not need a list tool to discover them.

11.1 searchKnowledge — vector search a knowledge base

@Tool: searchKnowledge
Parameter Type Required Description
knowledgeId string Yes Target knowledge base ID
query string Yes Search query (semantic similarity)
topK integer No Max chunks to return; defaults to rag.topK (config)

Returns: Top-k chunks with similarity score and text content. Empty result returns "未检索到相关文档片段". Access check: only own / subscribed / role-granted KBs are searchable; otherwise returns "没有权限访问该知识库".

Similarity threshold: Configured via rag.similarityThreshold (default 0.50). Chunks below the threshold are filtered out before being returned.


10. Terminal Management

10.1 Start Process

@Tool: startProcess

Start a terminal process or REPL session. Supports two modes: Shell mode (one-shot command like ls, cat) and REPL mode (long-running interactive session like python, node). REPL mode uses PTY (pseudo-terminal) when available for full terminal interaction.

Parameter Type Required Description
command string Yes Command to execute. Shell mode: any shell command. REPL mode: interpreter (e.g. python, node)
workingDir string No Working directory (default: ~/.loom/users/{username}/file/)
repl boolean No Whether REPL mode. true = long interactive session; false/omitted = one-shot command
timeout long No Wait timeout in milliseconds (default 30000ms)

10.2 Interact with Process

@Tool: interactWithProcess

Send input to a running REPL session and wait for response.

Parameter Type Required Description
sessionId string Yes Session ID (returned by startProcess)
input string Yes Input to send (newline auto-appended)
timeout long No Response wait timeout in ms (default 10000)

10.3 Read Process Output

@Tool: readProcessOutput

Read output from a running process. Supports three modes: new (unread content since last read, default), tail (last N lines), absolute (from character position N).

Parameter Type Required Description
sessionId string Yes Session ID
mode string No Read mode: new / tail / absolute
position int No Absolute character position (only when mode=absolute)
lines int No Number of lines (only when mode=tail, default 50)

10.4 Force Terminate

@Tool: forceTerminate

Force-terminate a managed terminal session.

Parameter Type Required Description
sessionId string Yes Session ID

10.5 List Sessions

@Tool: listSessions

List all active terminal sessions for the current user.


10.6 Get Process Info

@Tool: getProcessInfo

Get detailed info for a single session, including full output, process state, working directory, PTY mode, etc.

Parameter Type Required Description
sessionId string Yes Session ID

10.7 Send Signal

@Tool: sendSignal

Send a control signal to a terminal session. PTY mode supports: interrupt (Ctrl+C), eof (Ctrl+D), quit (Ctrl+\). Non-PTY mode only supports interrupt via destroy.

Parameter Type Required Description
sessionId string Yes Session ID
signal string Yes Signal type: interrupt / eof / quit

10.8 List System Processes

@Tool: listProcesses

List all running OS processes (like ps or Task Manager). Supports pagination.

Parameter Type Required Description
maxResults int No Max results per page (default 50, max 200)
page int No Page number, 0-based (default 0)

10.9 Kill Process

@Tool: killProcess

Force-terminate a system process by PID.

Parameter Type Required Description
pid long Yes Process ID
force boolean No Whether to use forceful termination (default true)

11. Sub-task & Schedule APIs

Sub-tasks are per-conversation (loom-subtask-{user}-{conv}-{taskId}); schedules are per-conversation (loom-sched-{user}-{conv}-{name}) and persist to the loom-agent-owned loom_scheduled_task table (migration). The flex.schedule.limits constraints are 10m min-interval / 72h max-lifetime / strict mode by default in the test app.

11.1 List Sub-task Limits

GET /spring/ai/loom/subtask/limits

Response: { "maxConcurrent": int, "maxHistory": int }

11.2 List Active Sub-tasks

GET /spring/ai/loom/subtask/list/active?conversationId=...

11.3 List Sub-task History

GET /spring/ai/loom/subtask/list/history?conversationId=...&limit=...

11.4 Kill a Sub-task

POST /spring/ai/loom/subtask/kill/{id}

Permission: Owner only. Cross-conversation kill returns 403.

11.5 Delete a Sub-task History Entry

DELETE /spring/ai/loom/subtask/history/{id}

11.6 List Schedule Limits

GET /spring/ai/loom/schedule/limits

Response: { "minInterval": "PT10M", "maxLifetime": "PT72H", "mode": "strict" } (ISO 8601 durations)

11.7 List All Schedules

GET /spring/ai/loom/schedule/list

Response: TaskInfo[] — all schedules visible to the current user.

11.8 Cancel a Schedule

POST /spring/ai/loom/schedule/cancel
Content-Type: application/json

{ "name": "loom-sched-alice-conv-1-防干眼提醒" }

Note: The body field is name, not taskName. The value is the FULL namespaced name (returned by list). Returns 403 for non-owner or non-existent task. Also deletes the corresponding loom_scheduled_task row so the ScheduleRestoreListener doesn't resurrect it on restart.

11.9 Cancel All Schedules for a Conversation

POST /spring/ai/loom/schedule/by-conversation/{conversationId}/cancel-all

11.10 Get Schedule Execution History

GET /spring/ai/loom/schedule/history/{name}

Permission: Owner only (returns 403 for non-owner).

11.11 Get Schedule History for a Conversation

GET /spring/ai/loom/schedule/history/by-conversation/{conversationId}

12. Admin APIs

All admin endpoints require the caller to have user_info.type = 'ADMIN'; non-admin access returns 403 ({"message": "无权限"}). The admin pages under admin/*.html are the canonical UI for these endpoints.

12.1 User Management

Method Path Purpose
GET /admin/users List all users
POST /admin/users Create user (body: CreateUserRequest — username, nickname, password, type). Returns 400 if name taken
DELETE /admin/users/{username} Delete user; refuses to delete self
GET /admin/users/{username}/conversations List a user's conversations (admin view)
GET /admin/users/{username}/roles Get user's roles
PUT /admin/users/{username}/roles Replace user's roles (body: SetUserRolesRequest — {"roleCodes": ["dev", "ops"]}). Returns 400 {"error":"用户不存在: xxx"} or {"error":"角色不存在: xxx"}
PUT /admin/users/{username}/password Reset a user's password (body: ResetPasswordRequest — {"newPassword": "..."}, min 6 chars). No old password required — an admin cannot know the user's chosen password. Returns 400 if the target is an ADMIN (resetting another admin would allow silent account takeover), if the user does not exist, or if the password is too short/long. The UI disables the button on ADMIN rows. Note: since the admin types the new password, the admin knows it — the modal tells the admin to relay it to the user.

12.2 Conversation Management (admin)

Method Path Purpose
GET /admin/conversations/{conversationId}/turns List every turn with token usage
GET /admin/conversations/{conversationId}/messages List raw ChatMessages
POST /admin/conversations/clean-batch Hard-delete soft-deleted conversations in batch

12.3 Token Usage Statistics & Logs

Method Path Purpose
GET /admin/stats/tokens/monthly?year=2026&month=7 Per-user aggregation for the month (defaults to current month if omitted)

12.4 Role / MCP / Skill / Knowledge Management

All under /admin/... and already documented in:

12.5 Admin UI Pages (admin/*.html)

Page Purpose
admin/console.html User list + role assignment + password reset + batch content cleanup. No longer links to user.html — the user-conversation entry point was consolidated into the Log page's second tab
admin/roles.html RBAC roles + grant MCP / Skill / Knowledge
admin/market-skills.html Approve / reject / directly CRUD Skill
admin/knowledge-market.html Approve / reject / directly CRUD Knowledge
admin/mcps.html Maintain Chinese descriptions for SDK MCP tools
admin/conversation.html Drill into any user's conversation turns (admin only)
admin/user.html?username=X Single-user view: 6-month usage + roles + conversation list; each row drills into conversation.html
admin/stats.html Two tabs: (1) Token usage stats (chart + table, usernames not clickable); (2) Conversation log — user list → user.html → conversation.html

The Log page's two tabs deliberately separate concerns: the Token tab is a pure statistics view and requires data for the current month, whereas the Conversation-log tab lists all users and works on a fresh deployment with no usage data.

All admin pages share a fixed left sidebar (see README "Admin Console" section).


13. Data Models

ChatRequestRecord

{
 "message": "string",
 "conversationId": "string",
 "mcps": ["string"],
 "enabledKnowledgeIds": ["string"],
 "fileIds": ["string"],
 "selectedSkillName": "string",
 "enabledToolGroups": ["string"]
}

ChatResponseRecord

{
 "content": "string",
 "reasoningContent": "string",
 "askUser": null,
 "subTaskEvent": null
}

askUser is null for normal frames; when non-null it carries an AskUserEvent question card (see § 3.1/3.2).

subTaskEvent is null for normal frames; when non-null it carries a SubTaskEvent describing a sub-task's lifecycle (started / completed / failed), which the frontend renders as a compact chip in the chat stream. Note the chip is a transient stream event — it is not persisted, so historical replay reconstructs it from messageType=TOOL_CALL entries instead (see § 2.2).

ConversationRecord

{
 "conversationId": "string",
 "title": "string"
}

UserRequestRecord

{
 "username": "string",
 "verified": "string"
}

UserResponseRecord

{
 "token": "string",
 "nickname": "string"
}

Note: The token is also set as an HttpOnly Set-Cookie header (loom-agent-session). The browser will automatically include this cookie in subsequent requests. Clients do not need to store or send the token manually.

FileRecord

{
 "id": "string",
 "knowledgeId": "string",
 "fileName": "string",
 "size": 0,
 "uploadTime": "2026-05-10T10:30:00",
 "path": "string",
 "usage": "conversation",
 "mimeType": "application/pdf"
}

KnowledgeRecord

{
 "id": "string",
 "username": "string",
 "name": "string"
}

McpRecord

{
 "name": "string",
 "title": "string",
 "version": "string",
 "description": "string",
 "defaultSelected": true,
 "tools": [
 {
 "name": "string",
 "description": "string"
 }
 ]
}

SkillRecord

{
 "name": "string",
 "description": "string",
 "load": true,
 "content": "string",
 "source": "USER_CREATED | MARKET_PULLED | ROLE_GRANTED"
}

The response shape is the same as the PUT request body (name / description / load / content), plus a source field set by the server to indicate where the skill was loaded from. source is one of USER_CREATED / MARKET_PULLED / ROLE_GRANTED.


14. Configuration Properties

All properties are prefixed with spring.ai.loom.agent in application.yml.

14.1 Basic Configuration

Property Type Default Description
spring.ai.loom.agent.defaultSystem string Skill discovery prompt Default system prompt
spring.ai.loom.agent.init boolean true Whether to initialize the ChatClient

14.2 RAG Configuration

Property Type Default Description
spring.ai.loom.agent.rag.similarityThreshold double 0.0 Vector retrieval similarity threshold
spring.ai.loom.agent.rag.topK int 4 Number of top results to retrieve

14.3 MCP Configuration

spring.ai.loom.agent.mcps is an array. Each entry contains:

Property Type Description
name string MCP server name
title string Display label
description string Description info
defaultSelected boolean Whether selected by default
tools[].name string Tool name
tools[].description string Tool description

14.4 Skill Configuration (no longer read from yml)

The spring.ai.loom.agent.skills[] yml block is no longer read. See §6 Skill Management for the database-driven flow. 6 system skills are seeded on first launch.

14.5 Vector Store Configuration (H2-backed JVector)

Property Type Default Description
spring.ai.loom.agent.jvector.m int 16 HNSW graph parameter M
spring.ai.loom.agent.jvector.efConstruction int 100 ef parameter at build time
spring.ai.loom.agent.jvector.efSearch int 10 ef parameter at search time

Persistence: vectors live in the H2 table loom_vector_store (embedding BLOB, little-endian float32) and are hydrated into the in-memory JVector HNSW graph on ApplicationReadyEvent — no boot-time re-embedding. The legacy ~/.loom/jvector-index/ json files are retired. Changing the embedding model invalidates stored vectors (dim-guarded rows are skipped with a WARN); wipe the table and re-upload knowledge documents.

14.6 Authentication Configuration

Property Type Default Description
spring.ai.loom.agent.auth.enabled boolean true Authentication master switch
spring.ai.loom.agent.auth.pathPatterns string[] ["/spring/ai/loom/**"] Paths requiring authentication (Ant-style patterns)
spring.ai.loom.agent.auth.excludePathPatterns string[] (see below) Paths excluded from authentication
spring.ai.loom.agent.auth.cookie.name string loom-agent-session Session cookie name
spring.ai.loom.agent.auth.cookie.maxAge int 86400 Cookie max age in seconds (24 hours)

14.7 File Storage Configuration

Property Type Default Description
spring.ai.loom.agent.usersBasePath string ~/.loom/users User-tree root; per-user sandbox = {usersBasePath}/{username}/file/

Files uploaded to the same directory with duplicate names are automatically renamed with a suffix: file.txt → file(1).txt → file(2).txt.

14.7.1 File Tool Configuration (IFileTool)

The file tool (IFileTool) is configured via spring.ai.loom.agent.file.* with a set of default safety/resource limits tailored for LLM tool-call scenarios.

Property Type Default Description
spring.ai.loom.agent.file.enabled boolean true Deprecated since M3 — no effect; IFileTool is a universal tool (always visible)
spring.ai.loom.agent.file.maxFileSize long 5242880 (5 MB) Per-call upper bound on file read/write size (bytes). Exceeding this is rejected outright, to avoid OOM and LLM context overflow.
spring.ai.loom.agent.file.maxMediaSize long 1048576 (1 MB) Upper bound on media files (images / audio). Base64-encoded size ≈ 4/3 of the original, so the limit is stricter than for text.
spring.ai.loom.agent.file.maxWalkDepth int 5 Upper bound on depth for directoryTree / recursive listing / search.
spring.ai.loom.agent.file.maxWalkEntries int 1000 Upper bound on entries returned per listDirectory / directoryTree call.
spring.ai.loom.agent.file.maxSearchResults int 500 Upper bound on searchFiles hit count.
spring.ai.loom.agent.file.deleteConfirmToken string I_CONFIRM_DELETE The deleteFileOrDirectory tool requires this exact string as an explicit argument before it executes (guards against accidental LLM deletions). Can be replaced with a shorter token (e.g. YES) to save tokens.
spring.ai.loom.agent.file.excludedDirs string[] [".git", "node_modules", "target", "build", "dist", ".idea", ".vscode", ".gradle", "out", "bin"] Directory names skipped during traversal (exact match, not glob). Keeps directoryTree / searchFiles from dumping tens of thousands of target/classes/*.class entries on a Spring Boot project.

Safety mechanisms:

  • All path resolution is delegated to PathSecurityUtils.assertInsideUserDir(resolved, userDir, mustExist), which uniformly handles:
  • .. traversal (a single Path.normalize defeats it)
  • Symlink escape (Path.toRealPath follows the chain) — even if the user drops a symlink inside userDir pointing at C:\Windows, the tool will not read through it
  • Size-bypass on case-insensitive filesystems (Windows / macOS)
  • Atomic writes: writeFile / editFile write to a .tmp file and then Files.move(ATOMIC_MOVE) it into place, so a mid-write power loss won't corrupt the target. Falls back to non-atomic replacement across volumes.
  • editFile uniqueness check: if oldText matches more than once in the file the call is rejected, forcing the LLM to provide more precise surrounding context.

14.8 End-to-End Deployment Configuration (ICompileAndDeployTool)

ICompileAndDeployTool performs the full deployment pipeline in a single LLM tool call: git clone → buildTool build (maven / npm / pip) → docker build → docker run → health check. Supports Maven, Node.js (backend and static-frontend → nginx), and Python projects. All settings live under spring.ai.loom.agent.compile.*.

Property Type Default Description
spring.ai.loom.agent.compile.enabled boolean true Deprecated since M3 — no effect; bean always created, visibility RBAC-gated via role_tool.tool_compile
spring.ai.loom.agent.compile.mavenHome string auto-discover Optional Maven install dir; falls back to maven.mavenHome and PATH
spring.ai.loom.agent.compile.dockerCmd string docker Optional override for the docker CLI binary
spring.ai.loom.agent.compile.imageTemplates map (6 pre-set templates) Pre-set base-image templates keyed by alias; see below
spring.ai.loom.agent.compile.extraRunArgs string[] [] Extra docker run args injected between --name and the image name

Base-image templates (built-in):

Alias Image Default ENTRYPOINT
java17 eclipse-temurin:17-jre-alpine ["java","-jar","app.jar"]
java21 eclipse-temurin:21-jre-alpine ["java","-jar","app.jar"]
nginx nginx:1.27-alpine ["nginx","-g","daemon off;"]
python3 python:3.12-slim ["python","app.py"]
node20 node:20-alpine ["node","dist/index.js"]
node20-serve nginx:1.27-alpine ["nginx","-g","daemon off;"]

Tool-call parameters (Map, case-insensitive, all optional except gitUrl, port, and containerPort):

  • gitUrl (required) — git repository URL
  • gitUsername / gitPassword — credentials for private repos
  • branch — branch to clone (defaults to remote HEAD)
  • port — host port the container will publish (the port the caller accesses via http://localhost:{port}/{healthPath})
  • containerPort — Container port the application listens on inside the container (required, no yml fallback; reference server.port in application.yml)
  • subDir — Subdirectory of a multi-module repo to deploy (required when root has no pom.xml — tool returns fail otherwise)
  • buildTool — Build tool / project type: maven / npm / npm-frontend / pip (optional; auto-detected from pom.xml / package.json / requirements.txt / pyproject.toml)
  • imageName / containerName — Docker image and container names (defaults derived from timestamp)
  • healthPath — both the health-check path and the access URL path (e.g. healthPath=sql-forge-demo → http://localhost:8080/sql-forge-demo)
  • baseImage — template alias (java17/java21/nginx/python3/node20/node20-serve) or full image name (e.g. openjdk:17-slim)
  • runCommand — string array overriding the template's default ENTRYPOINT (rare)

Example yml overriding the default templates:

spring:
 ai:
 loom:
 agent:
 compile:
 image-templates:
 java17:
 image: eclipse-temurin:17-jre-alpine
 command: [java, -jar, app.jar]
 nginx:
 image: nginx:1.27-alpine
 command: [nginx, -g, "daemon off;"]

Example tool invocation:

{
 "gitUrl": "https://gitee.com/wb04307201/sql-forge-demo.git",
 "port": 8081,
 "containerPort": 8080,
 "subDir": "sql-forge-web",
 "baseImage": "java17",
 "healthPath": "sql-forge-demo"
}

14.9 Git Configuration (IGitTool)

IGitTool provides Git operations (init, clone, status, commit, branch, etc.) via Eclipse JGit. The bean is always created; visibility is RBAC-gated — an admin must grant tool_git to a role (/admin/roles/{code}/tools, persisted in role_tool). The git.enabled yml flag is deprecated (no effect).

Property Type Default Description
spring.ai.loom.agent.git.enabled boolean false Deprecated since M3 — no functional effect; visibility is RBAC-gated via role_tool.tool_git
spring.ai.loom.agent.git.username string — Git username for remote authentication
spring.ai.loom.agent.git.token string — Git token / password for remote authentication

Example:

spring:
 ai:
 loom:
 agent:
 git:
 username: your-username
 token: your-token
# Note: no 'enabled' flag — grant tool_git to a role in the admin console instead

14.10 Maven Build Configuration

Property Type Default Description
spring.ai.loom.agent.maven.enabled boolean false Deprecated since M3 — no functional effect; the bean is created when maven-invoker is on the classpath (a default lib dependency) and visibility is RBAC-gated via role_tool.tool_maven. Compile/package for deployment scenarios is handled by ICompileAndDeployTool
spring.ai.loom.agent.maven.mavenHome string — Maven install directory. When empty, the tool auto-discovers: it tries the MAVEN_HOME / M2_HOME environment variables first, then scans common Windows paths (e.g. C:\developer\apache-maven-*, C:\Program Files\Apache Maven). Auto-discovery does not rely on the system PATH, so a broken or shadowing mvn wrapper (e.g. a global npm mvn) won't cause maven-invoker to throw Error configuring command line.
spring.ai.loom.agent.maven.localRepository string — Local repository path (uses the default path when empty)
spring.ai.loom.agent.maven.maxOutputLines int 200 Maximum output lines (truncated when exceeded)
spring.ai.loom.agent.maven.defaultTimeoutMs long 300000 Default execution timeout in milliseconds (5 minutes)

All Maven tool operations are scoped to {usersBasePath}/{username}/file/; paths outside that range are rejected.

Troubleshooting tip — MavenInvocationException: Error configuring command line: this means maven-invoker could not find a usable mvn / mvn.cmd. The tool startup log prints the resolved mavenHome together with a diagnostic hint listing every path it searched, the environment variables it looked at, and how to fix it. The most common cause is a broken or shadowing mvn on PATH (e.g. a global npm mvn wrapper); in that case, explicitly set spring.ai.loom.agent.maven.mavenHome in application.yml to point at the real Maven install directory to bypass it.

Troubleshooting tip — "file is locked" errors when deleting the project directory on Windows: in older versions this was caused by maven-invoker 3.3.0 / plexus-utils 3.3.0 because (a) the JVM shutdown hook they register on the exception/cancel path never releases the held Process reference, and (b) Invoker.execute does not expose the child-process handle, so it cannot propagate cancel/timeout down to the mvn child process. The result: a cancelled or timed-out Maven call would leave the mvn child running and continuing to hold mmap handles on target/classes and ~/.m2/repository/*.jar, locking those files on Windows. The new version no longer uses Invoker.execute to run the process — it forks mvn directly with ProcessBuilder, does a clean timeout via Process.waitFor(timeout, unit), then calls Process.destroyForcibly and explicitly closes the streams on timeout. No JVM shutdown hook is registered any more, so the mvn child is always killed on timeout/cancel. If you still see locks after upgrading, it is most likely an orphan mvn process left behind by a previous JVM — find it with tasklist /FI "IMAGENAME eq cmd.exe" and taskkill /F /PID <pid> it.

14.11 Tool Group Switches (deprecated)

Since M3, the *.enabled switches below no longer gate tool beans. All always-on I*Tool beans are created unconditionally (plus IHtmlRenderTool, gated only by playwright on the classpath). Visibility is governed by universal tools (@ToolGroup(defaultGranted=true) — visible to every logged-in user) vs RBAC tools (tool_git / tool_maven / tool_compile / tool_render, granted per role via /admin/roles/{code}/tools, persisted in role_tool). See TOOLS.md §1 for the full model. The properties remain for backward compatibility only.

Property Type Default Description
spring.ai.loom.agent.time.enabled boolean true Deprecated — no effect (ITimeTool is universal)
spring.ai.loom.agent.file.enabled boolean true Deprecated — no effect (IFileTool is universal)
spring.ai.loom.agent.skill.enabled boolean true Deprecated — no effect (ISkillTool is universal)
spring.ai.loom.agent.git.enabled boolean false Deprecated — no effect; IGitTool bean always created, visibility RBAC-gated via role_tool.tool_git
spring.ai.loom.agent.maven.enabled boolean false Deprecated — no effect; IMavenTool bean created when maven-invoker on classpath, visibility RBAC-gated via role_tool.tool_maven
spring.ai.loom.agent.git.username string — HTTP(S) Git auth username (clone/pull/push)
spring.ai.loom.agent.git.token string — HTTP(S) Git auth token / password
spring.ai.loom.agent.gitUsername string — Legacy top-level alias, equivalent to git.username
spring.ai.loom.agent.gitToken string — Legacy top-level alias, equivalent to git.token

Example — expose the Git tool to a role: no yml change needed; in the admin console (角色管理 → base → 编辑/授权 → 授权本地工具组) grant tool_git to the role.

To replace a tool implementation, provide your own @Bean IGitTool / @Bean IMavenTool — @ConditionalOnMissingBean always gives user-provided beans precedence.


Appendix: Endpoint Summary

# Method Path Description
1 POST /spring/ai/loom/user/isAutoLogin Check auto-login status
2 POST /spring/ai/loom/user/login User login (sets session cookie)
2a POST /spring/ai/loom/user/logout User logout (invalidates session)
3 GET /spring/ai/loom/conversation List conversations
4 GET /spring/ai/loom/conversation/{id} Get conversation history
5 DELETE /spring/ai/loom/conversation/{id} Delete conversation
6 POST /spring/ai/loom/stream SSE streaming chat
7 POST /spring/ai/loom/file/upload Upload file
8 GET /spring/ai/loom/file or /spring/ai/loom/file/tree List file tree (directory tree JSON)
8a GET /spring/ai/loom/file/by-path/view View file by path (redirects)
8b GET /spring/ai/loom/file/by-path/download Download file by path (redirects)
8c GET /spring/ai/loom/file/{id}/download Download file by ID (binary stream)
9 GET /spring/ai/loom/knowledge/checkKnowledgeUpload Check knowledge base status
10 GET /spring/ai/loom/knowledge List knowledge bases
11 PUT /spring/ai/loom/knowledge Create knowledge base
12 DELETE /spring/ai/loom/knowledge/{id} Delete knowledge base (cascade)
13 POST /spring/ai/loom/knowledge/{id}/upload Upload file to knowledge base
14 GET /spring/ai/loom/knowledge/{id}/file List files in knowledge base
15 DELETE /spring/ai/loom/knowledge/{id}/file/{fileId} Delete file from knowledge base
15a GET /spring/ai/loom/api/knowledge-market Browse approved market knowledge (kept v1 leg; v2 GET /market-knowledge is the primary list)
15b POST /spring/ai/loom/api/knowledge-market/{marketId}/pull Subscribe to market knowledge (APPROVED-only, else 403)
15c POST /spring/ai/loom/api/knowledge/{knowledgeId}/submit Submit knowledge to market (→ PENDING)
15d DELETE /spring/ai/loom/api/knowledge-market/{marketId} Withdraw market submission
15e POST /spring/ai/loom/admin/market-knowledge/{id}/approve Admin approve market submission (v2 admin router)
15f POST /spring/ai/loom/admin/market-knowledge/{id}/reject Admin reject market submission (comment required)
15g GET /spring/ai/loom/api/knowledge-market/my-pulled List my subscribed market knowledge
15h GET /spring/ai/loom/api/knowledge-market/my-submitted List my market submissions
16 GET /spring/ai/chat/loom/mcp Get MCP servers and tools
17 GET /spring/ai/loom/skill List all skills
18 PUT /spring/ai/loom/skill Create or update a skill
19 GET /spring/ai/loom/skill/{name} Get skill details
20 DELETE /spring/ai/loom/skill/{name} Delete a skill