Skip to main content

Documentation Index

Fetch the complete documentation index at: https://ahvn.top/llms.txt

Use this file to discover all available pages before exploring further.

Build sub-Linear app in sub-linear time.
In this workshop, you build Sublinear from scratch: a project and issue tracker where one agent can create records, update work, answer status questions, and run semantic issue search through HeavenBase MCP tools. The whole app is one Python file. It defines the data model, drops and recreates the demo workspace for repeatable runs, seeds allowed labels, then lets the agent operate the workspace through MCP.
Set OPENROUTER_API_KEY before running the workshop. By default, Sublinear uses OpenRouter for both chat (deepseek/deepseek-v4-flash) and embeddings (openai/text-embedding-3-small), which may incur small usage costs (< $0.01).

1. Initialization

Start with imports, constants, and backend configuration. For simplicity, we only use SQLite + In-memory backends, but you can swap in other backends or add more as needed. The main backend stores entities and supports SQL queries; the vec backend stores issue embeddings and supports vector search.
import heavenbase as hb
from heavenbase.utils import Any, LLMSession

WORKSPACE_ID = "sublinear"
TOOLKIT_NAME = "sublinear-mcp"
PRIORITY_RANKS = {
    "urgent": 1,
    "high": 2,
    "medium": 3,
    "low": 4,
    "none": 5,
}
DATA_DIR = "./.data/sublinear/"
hb.utils.touch_dir(DATA_DIR)
BACKENDS = {
    "main": {
        "type": "sqlite",
        "name": "main",
        "database": f"file:{hb.utils.pj(DATA_DIR, 'sublinear-main.db')}",
    },
    "vec": {"type": "inmem", "name": "vec"},
}
Then for convenience, we start by defining some helper functions including label normalization, priority ranking, text embedding, and issue text construction. The workshop uses the embed preset, which resolves to OpenRouter’s OpenAI-compatible openai/text-embedding-3-small route by default. You can switch it to embed-local to run small embedding models locally on Ollama / LM Studio / vLLM.
def label_id(name: str) -> str:
    """Return a readable label identity from the label name."""
    return (name or "").strip().lower().replace(" ", "-")


def rank_priority(priority: str = "medium") -> int:
    """Return sortable priority rank, where urgent is first."""
    return PRIORITY_RANKS.get((priority or "medium").lower(), PRIORITY_RANKS["medium"])


def embed_text(text: str) -> list[float]:
    """Embed task text with the configured embedding preset."""
    embedder = hb.LLM(preset="embed")
    vector = embedder.embed(text or "")
    return [float(item) for item in vector]


def issue_text(title: str, description: str, tags: list[str] | None = None) -> str:
    """Build the issue text used for vector indexing."""
    return " | ".join([title or "", description or "", ", ".join(tags or [])])

2. Entity Definition

To build an app from scratch, start by clarifying your data model in a declarative fashion similar to Pydantic. In our design of sublinear, we have projects, milestones, and issues. Projects group work and set goals. Milestones are project checkpoints that can be used for progress tracking. Issues are the individual work items that can be assigned, labeled, tagged, and embedded for semantic search. We also have a Label entity to store the allowed label vocabulary and a View entity to store saved filters and display preferences. The Label entity:
class Label(hb.Entity):
    """Allowed project and issue label."""

    object_id = hb.field(hb.Identifier).compute(label_id, inputs=["name"])
    name = hb.field(hb.ShortText).desc("Label name")
    color = hb.field(hb.ShortText).default("gray").desc("Label color for UI display")
    description = hb.field(hb.LongText).default("").desc("Label description for agent reasoning")
Use .compute to annotate fields that are computed from an arbitrary Python function. The function will take the initialization inputs as keyword arguments and return the transformed value that will be stored on the field.
The Milestone entity:
class Milestone(hb.Entity):
    """Sublinear project milestone used for progress summaries."""

    project_id = hb.field(hb.Identifier).desc("Owning project object_id")
    name = hb.field(hb.ShortText).desc("Milestone name")
    description = hb.field(hb.LongText).default("").desc("Milestone scope")
    status = hb.field(hb.ShortText).default("planned").desc("planned, active, done, or skipped")
    target_date = hb.field(hb.Date).optional().desc("Milestone target date")
    sort_order = hb.field(hb.Integer).default(100).desc("Display order inside the project")
The Project entity:
class Project(hb.Entity):
    """Sublinear project containing goals and issue work."""

    name = hb.field(hb.ShortText).desc("Project display name")
    summary = hb.field(hb.LongText).default("").desc("Project overview")
    owner = hb.field(hb.ShortText).default("unassigned").desc("Primary owner")
    status = (
        hb.field(hb.ShortText).default("active").desc("planned, active, paused, done, or archived")
    )
    priority = hb.field(hb.ShortText).default("medium").desc("urgent, high, medium, low, or none")
    target_date = hb.field(hb.Date).optional().desc("Project target date")
    labels = (
        hb.field(hb.Array[hb.ShortText])
        .default([])
        .store(to="main", strategy=hb.SideTable)
        .desc("Project label object_id values from Label rows")
    )
    goals = hb.field(hb.LongText).default("").desc("Plain-language project goals")
Here we see a new concept strategy, which is used to determine how the field is stored on the backend. Even if the same type storing in the same backend, different strategies will result in different physical storage layouts. For example, for the hb.SideTable strategy, the array will be stored as a separate table with a foreign key column to the main table, and each item will be stored as a separate row.
The View entity:
class View(hb.Entity):
    """Saved Sublinear filter and display configuration."""

    name = hb.field(hb.ShortText).desc("View display name")
    target_entity = hb.field(hb.ShortText).default("issue").desc("Entity this view queries")
    owner = hb.field(hb.ShortText).default("team").desc("View owner")
    filter_json = hb.field(hb.Json).default({}).desc("HeavenBase JSON query filter")
    group_by = hb.field(hb.ShortText).default("status").desc("Preferred grouping field")
    order_by = hb.field(hb.ShortText).default("priority_rank").desc("Preferred order field")
    display = (
        hb.field(hb.Array[hb.ShortText])
        .default(["key", "title", "status", "priority"])
        .desc("Shown fields")
    )
    shared = (
        hb.field(hb.Boolean).default(True).desc("Whether the whole workspace should use the view")
    )
As a comparison, display is also an array field, but it does not specifies strategy, so it uses the default hb.InlineColumn strategy for arrays, which is a simple inline column in the main table (as TEXT, JSON/JSONB or ARRAY according to the specific backends).
The Issue entity (the most complex one):
class Issue(hb.Entity):
    """Sublinear issue with Linear-inspired properties and vector search."""

    key = hb.field(hb.ShortText).desc("Human issue key such as S1")
    project_id = hb.field(hb.Identifier).desc("Owning project object_id")
    milestone_id = hb.field(hb.Identifier).optional().desc("Milestone object_id")
    title = hb.field(hb.ShortText).desc("Issue title")
    description = hb.field(hb.LongText).default("").desc("Issue details")
    status = (
        hb.field(hb.ShortText)
        .default("todo")
        .desc("backlog, todo, in-progress, blocked, done, or canceled")
    )
    priority = hb.field(hb.ShortText).default("medium").desc("urgent, high, medium, low, or none")
    priority_rank = (
        hb.field(hb.Integer)
        .compute(rank_priority, inputs=["priority"])
        .desc("Sortable priority rank")
    )
    assignee = hb.field(hb.ShortText).default("unassigned").desc("Current assignee")
    estimate = hb.field(hb.Integer).default(0).desc("Small integer effort estimate")
    labels = (
        hb.field(hb.Array[hb.ShortText])
        .default([])
        .store(to="main", strategy=hb.SideTable)
        .desc("Issue label object_id values from Label rows")
    )
    tags = (
        hb.field(hb.Array[hb.ShortText])
        .default([])
        .store(to="main", strategy=hb.SideTable)
        .desc("Free-form issue tags")
    )
    blocked_by = hb.field(hb.Array[hb.ShortText]).default([]).desc("Issue keys blocking this work")
    due_date = hb.field(hb.Date).optional().desc("Optional due date")
    created_at = hb.field(hb.Timestamp["s"]).optional().desc("UTC+0 epoch seconds")
    updated_at = hb.field(hb.Timestamp["s"]).optional().desc("UTC+0 epoch seconds")
    search_text = (
        hb.field(hb.LongText)
        .compute(issue_text, inputs=["title", "description", "tags"])
        .desc("Text used to compute issue embedding")
    )
    emb = (
        hb.field(hb.Vector[hb.LLM(preset="embed").dim])
        .compute(embed_text, inputs=["search_text"])
        .query_compute(embed_text)
        .store(to="vec", strategy=hb.VectorIndex)
        .desc("Issue embedding stored on the vector backend; semantic near accepts text queries")
    )
Here we see another new concept query_compute, which is used to transform the query-time arguments. For example, when writing query emb.near("Hello"), the query args "Hello" will be processed by the embed_text and become a vector to match to the nearest embeddings.

3. Workspace Construction

Create the workspace, register the entities, and seed the label vocabulary. The agent queries these rows and stores label object_id values on projects and issues. Label and tag arrays are routed to SQLite side tables, so analytical filters such as labels.array_contains("debugging") work without a separate search backend.
def sublinear_workspace(*, reset: bool = False) -> hb.HeavenBase:
    """Open the Sublinear workspace and register entity classes."""
    if reset:
        hb.HeavenBase(WORKSPACE_ID, backends=BACKENDS).drop()
    ws = hb.HeavenBase(WORKSPACE_ID, backends=BACKENDS)
    for entity in (Label, Project, Milestone, Issue, View):
        ws.register(entity)
    return ws


ws = sublinear_workspace(reset=True)
The setup calls sublinear_workspace(reset=True), which drops the demo workspace before each run. Keep this reset while following the workshop, and remove it when you want Sublinear data to persist.
Now, let’s seed the workspace with some label vocabulary.
ws.upsert_many(
    Label,
    [
        {"name": "research", "color": "blue", "description": "Research and design such as user survey and prototyping"},
        {"name": "design", "color": "purple", "description": "Design-related tasks"},
        {"name": "coding", "color": "yellow", "description": "Implementation and coding tasks"},
        {"name": "debugging", "color": "red", "description": "Bug fixing and debugging tasks"},
        {"name": "testing", "color": "orange", "description": "Testing and quality assurance tasks"},
        {"name": "documentation", "color": "lightblue", "description": "Documentation and writing tasks"},
        {"name": "low-effort", "color": "gray", "description": "Low-effort tasks under 4 units"},
        {"name": "medium-effort", "color": "lightgray", "description": "Medium-effort tasks from 4 to 15 units"},
        {"name": "high-effort", "color": "white", "description": "High-effort tasks over 15 units"},
    ],
)

4. Sublinear Agent

When the workspace is constructed, we can directly expose it to the agent through MCP. While any Harness (Claude Code, Codex, Copilot, etc.) can be used to create the agent, we use HeavenBase’s simple LLMSession to create the agent. To add MCP to a session, use session.add_mcp(...). Meanwhile, we can use a simple system prompt to guide the agent.
def sublinear_system_prompt() -> str:
    return """\
You are Sublinear Agent. Use HeavenBase MCP for all reads and writes.
- Inspect entities before writing. Store dates as YYYY-MM-DD.
- Create rows with upsert: omit object_id, provide name. Patch existing rows with set after querying the row.
- Patches should change only fields the user requested unless the user asks to reclassify labels or tags.
- For issue creates: project_id=Project.object_id, name=key, key=key. Query all Label rows, infer 1-3 labels from the full label set, store Label.object_id values, and add readable keywords to tags.
- If an issue mentions bugs, bug fixing, or debugging, include the debugging label.
- Never write priority_rank, search_text, or emb.
- Semantic search must query Issue with {"near":{"field":"emb","query":"text","top_k":5}}; never send vectors.
- If a tool call errors, retry with corrected arguments before answering. Answer only from successful tool results.
- Keep replies concise and plain ASCII. Avoid emoji and long tables.
""".strip()


def sublinear(question: str, llm: Any = None) -> str:
    """Perform a natural language question or command over the Sublinear workspace."""
    session = LLMSession(
        llm or hb.LLM(preset="chat", temperature=0.0, cache=False, max_tokens=4096)
    )
    session.add_mcp(
        ws.to_mcp(name=TOOLKIT_NAME, profile="agent").to_fastmcp(), name="sublinear-mcp-client"
    )
    final = session.send(question, system=sublinear_system_prompt(), max_tool_turns=20)
    return final.get("content") or ""
Now sublinear is already an agentic function that can answer questions and execute commands over the Sublinear workspace. The agent sees one MCP surface in each session: the HeavenBase workspace toolkit. It can write records, query structured rows, and run semantic near searches until the session ends with a content or reaching max_tool_turns. Here are the list of interfaces available to the agent (same as the HeavenBase MCP page):
ToolWhat it offers
define_entityCreates an entity definition from a JSON-compatible schema.
list_entitiesLists the workspace entities the agent can inspect.
describe_entityReturns one entity’s fields, logical types, and routing plan.
upsertInserts or replaces one row for one entity.
getFetches one row by object ID.
setPatches one row and returns the updated row.
countCounts rows for one entity.
queryRuns a JSON query with filters, projections, sorting, and limits.
explainShows the route and handler plan for a query.
Among the operations, upsert is best for creates and full-row replacement. For agent edits, set is more convenient because it patches only the changed fields after the agent queries the existing row. For semantic search, the important point is that the model sends text, not vectors. The query_compute(embed_text) hook on Issue.emb turns the query string into a vector before HeavenBase routes the near operation to vec; HeavenBase then hydrates the matching issue rows from SQLite. Example semantic search query:
{
    "near": {
        "field": "emb",
        "query": "launch readiness, debugging, and final polish",
        "top_k": 5
    },
    "select": ["key", "title", "status", "labels", "score"]
}

5. Try It Out

Run python workshops/sublinear/sublinear_app.py from the docs repo root to try the sublinear agent. The following script sends five user requests to Sublinear. Each request uses a new session, but all sessions share the same persistent HeavenBase workspace. The requests include: initializing a project, adding issues, self-assigning an issue, counting debugging issues, and semantic searching for “launch readiness, debugging, and final polish”.
if __name__ == "__main__":
    print(sublinear("""\
[USER: Mira]
Add a new "HB-GUI" project with high priority and ddl June 1st, 2027.
Project Goal: Create a user-friendly modern GUI for a specific app HB.
""".strip()))

    print(sublinear("""\
[USER: Mira]
Add the following issues in order to the "HB-GUI" project:
1. S1: Survey GUI techstacks 2026, design at least 3 different plans (due 2026-06-17)
2. S2: Finalize plan and start implementing a prototype (due 2026-08-03)
3. S3: Test prototype with 5 users and iterate based on feedback, fix bugs (due 2026-10-03)
4. S4: GUI UX optimization and polish (due 2026-11-03)
5. S5: Research about parallelism and optimize implementation (due 2026-11-03)
6. S6: Final testing and debugging (due 2026-12-03)
7. S7: Prepare launch materials (docs, tutorial, etc.) and launch (due June 1st, 2027)
""".strip()))

    print(sublinear("""\
[USER: Bob]
Self-assign S1, with estimating effort 3 units.
""".strip()))

    print(sublinear("""\
[USER: Alice]
How many debugging tasks are currently planned, and which ones are they?
""".strip()))

    print(sublinear("""\
[USER: Carol]
Use semantic search over issue embeddings for "launch readiness, debugging, and final polish".
Which planned issues are the closest matches, and why? Report only the relevant ones.
""".strip()))

5.1. Expected Behavior

The five calls intentionally use separate LLMSession instances. This shows that the agent is not relying on chat memory; each turn recovers context by querying the same HeavenBase workspace through MCP. During the run, the agent should:
  1. Create the HB-GUI project with upsert, omitting object_id and letting HeavenBase hash the project name.
  2. Query the project and label vocabulary, then create issues S1 through S7 with due dates, labels, tags, and computed embeddings.
  3. Patch S1 with set so only assignee and estimate change while computed fields remain consistent.
  4. Answer the debugging-count question from stored rows, usually by filtering Issue.labels for the debugging label ID. JSON specs may use array_contains; HeavenBase also normalizes contains on array fields to the same operation.
  5. Answer the semantic-search question with a text near query against Issue.emb, returning the closest related issues with their scores.
A successful run creates the HB-GUI project, adds seven issues with inferred label IDs, assigns S1 to Bob with set, answers that the debugging work includes S3 and S6, and uses near.query text on the vector field to rank launch/debugging/polish related issues such as S6, S7, and S2.

5.2. Example Replies

Your model may phrase the replies differently, but a successful run should be this concrete:
PromptExample reply
Add the HB-GUI projectCreated HB-GUI as a high-priority active project targeting 2027-06-01.
Add issues S1-S7Added S1-S7 to HB-GUI with due dates, inferred label IDs such as research, design, coding, debugging, testing, and documentation, plus readable tags.
Self-assign S1S1 is now assigned to Bob with estimate 3.
Count debugging work2 debugging tasks are planned: S3 and S6.
Semantic searchClosest matches: S6 final testing/debugging, S4 UX polish, and S3 user-test bug fixes.

The important part is not the exact wording of the responses. The app demonstrates that one agent can manipulate structured records, compute fields, route vectors to the vector backend, and query the same workspace for analytical answers.

Further Exploration

Related resources:
  • HeavenBase MCP - expose a workspace over MCP
  • First LLM - configure the chat preset used by Sublinear
  • First MCP - persist and reuse registered Toolkits
  • Entities - class syntax, defaults, compute hooks, and logical types
  • Routing - field-level backend placement
  • Query - JSON near queries and filtering