Agent Spaces
Agent Spaces are named, admin-authored personas that scope an AI ask. Where Agent Skills codify individual workflows, a space codifies a viewpoint: system prompt, cube allowlist, skill allowlist, suggested prompts — enforced server-side so no matter what the caller sends, the LLM sees the persona voice + the persona’s cubes + the persona’s skills.
Persisted as JSON under saiku-home/agent-spaces/. The launcher scans
lazily on mtime signature (same model as skills — no watcher thread).
Shipped in saiku v4.7 as saiku#1440.
What a space enforces
File format
{ "id": "foodmart-sales-analyst", "name": "FoodMart Sales Analyst", "description": "Weekly and monthly sales rollups over the FoodMart Sales cube.", "systemPrompt": "You are the FoodMart Sales Analyst. Prefer weekly and monthly time grain unless the user asks otherwise. Lead with the top three lines by absolute value. Flag any figure that swings by more than 20% versus the prior period. Be analytical, brief, and numbers-first.", "cubeAllowlist": [ {"connectionName": "unknown_foodmart", "catalog": "FoodMart", "schema": "FoodMart", "cubeName": "Sales"} ], "skillAllowlist": ["weekly-foodmart-rollup"], "suggestedPrompts": [ "How did Store Sales track last week vs the prior week?", "Break down Store Sales by Product Family for Q4.", "/weekly-foodmart-rollup" ]}| Field | Required | Type | Notes |
|---|---|---|---|
id | yes | string | kebab-case, [a-z][a-z0-9-]{0,63}. Path segment in /ai/spaces/{id}/ask. |
name | yes | string | Display name for the catalogue and sidebar. |
description | no | string | One-line summary shown in the space picker. |
systemPrompt | no | string | Prepended to the built-in SYSTEM_PROMPT on every ask. |
cubeAllowlist | yes | AiCubeRef[] | At least one entry. Refs outside this list return 403 FORBIDDEN. |
skillAllowlist | no | string[] | Filters slash-routing + LLM catalogue. Empty = all skills allowed. |
suggestedPrompts | no | string[] | Free-form quick-start questions the UI can render. |
Unknown top-level keys are rejected so a typo (sytemPrompt)
surfaces as UNKNOWN_FIELD rather than being silently dropped.
Cube enforcement
-
Caller POSTs to
/ai/spaces/{id}/askwith an optionalcubefield:Terminal window curl -sS -X POST -H 'Content-Type: application/json' \-u admin:admin \http://localhost:8080/saiku/api/ai/spaces/foodmart-sales-analyst/ask \-d '{"question": "How did Store Sales track last week?"}' -
If
cubeis omitted, the space’s first allowlisted entry is used. -
If
cubeis supplied, it must match a space allowlist entry on all four coordinates (connectionName,catalog,schema,cubeName) or the call returns:HTTP 403 Forbidden{"degraded": true,"reason": "FORBIDDEN: cube OtherCube is not in space 'foodmart-sales-analyst' allowlist"}
This is deliberate: a UI that hands the server a stale cube ref should get corrected, not silently reinterpreted onto the default.
System prompt injection
The space’s systemPrompt is prepended to the built-in SYSTEM_PROMPT
on the provider side. The full assembled system message the LLM sees:
You are a Mondrian OLAP analyst assistant scoped to a single cube. …[…the built-in tool-choice rails…]
Agent space persona:You are the FoodMart Sales Analyst. Prefer weekly and monthly timegrain unless the user asks otherwise. Lead with the top three lines byabsolute value. Flag any figure that swings by more than 20% versus theprior period. Be analytical, brief, and numbers-first.
Cube schema:{ … the AiSchema JSON … }
Cube ref to echo: { … }The user’s history and question fields ride below all of that as
regular messages — nothing they can inject re-writes the persona.
Skill filter
The skill catalogue is filtered to the space’s
skillAllowlist before it reaches the LLM:
- The
skillAllowlistis empty → all skills flow through. - The
skillAllowlistnames specific skills → only those show up in the LLM system prompt AND only those slash-expand.
An ask like /some-other-skill for Q4 in a space that doesn’t
allowlist some-other-skill falls through as a raw ask — the LLM sees
the message literally, with no expansion. The routing decision is
one-sided (allowlist deny), never partial.
Suggested prompts
Every space carries a suggestedPrompts list — 3-6 authored
quick-start questions the UI surfaces as chips:
"suggestedPrompts": [ "How did Store Sales track last week vs the prior week?", "Break down Store Sales by Product Family for Q4.", "Which Product Department is up the most month-over-month?", "/weekly-foodmart-rollup"]Slash-command entries are legal and encouraged — a suggested prompt
that starts with / invokes the named skill directly.
REST surface
GET /rest/saiku/api/ai/spaces
Catalogue of {id, name, description, suggestedPrompts} summaries.
curl -sS -u admin:admin \ http://localhost:8080/saiku/api/ai/spaces | jq{ "spaces": [ { "id": "foodmart-sales-analyst", "name": "FoodMart Sales Analyst", "description": "Weekly and monthly sales rollups over the FoodMart Sales cube.", "suggestedPrompts": ["How did Store Sales track last week?", "…"] } ]}GET /rest/saiku/api/ai/spaces?errors=true
Same, plus an errors[] array. Stable error codes:
| Code | When |
|---|---|
EMPTY_SPACE | File is empty or all whitespace. |
MALFORMED_JSON | JSON parser rejected the body. |
MISSING_FIELD | Required field (id, name) not present. |
BLANK_FIELD | Required field present but empty / whitespace-only. |
TYPE_MISMATCH | Field present but wrong type. |
INVALID_ID | id doesn’t match [a-z][a-z0-9-]{0,63}. |
EMPTY_ALLOWLIST | cubeAllowlist present but empty — space would be unusable. |
INVALID_CUBE_REF | Allowlist entry missing a coordinate (connectionName, etc.). |
UNKNOWN_FIELD | Frontmatter contains a field not in the schema. |
DUPLICATE_ID | Two files declared the same id. |
IO_ERROR | Could not read the file (filesystem-level). |
GET /rest/saiku/api/ai/spaces/{id}
Full record — includes systemPrompt and cubeAllowlist that the
summary omits. Used by the admin UI when editing a persona.
POST /rest/saiku/api/ai/spaces/{id}/ask
Space-scoped ask. Body shape mirrors /ai/ask but the
cube field is optional:
curl -sS -X POST -H 'Content-Type: application/json' \ -u admin:admin \ http://localhost:8080/saiku/api/ai/spaces/foodmart-sales-analyst/ask \ -d '{"question": "Break down Store Sales by Product Family for Q4"}'Response envelope is the standard AiAskApi.AskResponse
— model, request (the AiQueryRequest the model emitted), and
degraded/reason on error.
POST /rest/saiku/api/ai/spaces/{id}/ask/stream
Streaming variant of the space-scoped ask. Emits the same Server-Sent
Events schema as /ai/ask/stream
(model → intent → chunk → final) with the persona scope applied —
the client sees identical wire events whether it hits /ai/ask/stream or
this space-scoped mirror. Space-not-found and out-of-allowlist cube
outcomes surface as an error event followed by a degraded final, so
the reader handles scope failures and provider failures the same way.
curl -sS -N -X POST -H 'Content-Type: application/json' \ -u admin:admin \ http://localhost:8080/saiku/api/ai/spaces/foodmart-sales-analyst/ask/stream \ -d '{"question": "Break down Store Sales by Product Family for Q4"}'POST /rest/saiku/api/ai/spaces/refresh
Force a rescan (bypasses the mtime signature check).
Authoring spaces in the admin panel
Spaces no longer have to be hand-authored JSON. Admin → Agent spaces is a full editor: write the system prompt, tick the cubes in the allowlist (backed by live cube discovery, so you can only allow cubes that exist), list the skills and suggested prompts, and save. Deleting a space removes its JSON file.
The editor is backed by an admin-gated CRUD surface. These endpoints
require ROLE_ADMIN and return the full persona (system prompt +
cube allowlist included, unlike the redacted public catalogue above):
| Method + path | Purpose |
|---|---|
GET /rest/saiku/admin/agent-spaces | List every persona in full, for editing. |
GET /rest/saiku/admin/agent-spaces/errors | Parse errors for malformed files. |
PUT /rest/saiku/admin/agent-spaces/{id} | Create or replace a persona. The id is validated (kebab-case, path-traversal guarded) before the JSON file is written. |
DELETE /rest/saiku/admin/agent-spaces/{id} | Remove a persona and its file. |
Embedding a space
<saiku-embed kind="ai" space="foodmart-sales-analyst"> drops a
persona-scoped assistant into any page. The cube allowlist and system
prompt are enforced server-side exactly as they are for the REST ask, so
an embedded assistant can’t be steered off its persona. See the
embed guide.
Bundled examples
Fresh Saiku installs stage two working personas:
- FoodMart Sales Analyst — analytical, brief, numbers-first.
weekly-foodmart-rollupin its skill allowlist so/weekly-foodmart-rollupis available as a slash command. - FoodMart Finance Ops — cautious, precise, margin-focused. Empty
skillAllowlist= all skills allowed.
Both scope to the FoodMart Sales cube — a fresh demo has personas ready to click without any operator authoring.
Non-goals for v1
- Per-user or per-role scoping. Spaces are per-launcher in v1; multi-tenant workspaces can layer scoping by mapping workspace directories onto per-workspace registry roots — deferred.
- Per-space data-scope override. Embedded queries already enforce forced row-level filters (apply-or-fail-closed — see the embed guide), but pinning RLS filters onto space-scoped asks specifically is still tracked with the Ossie RLS work in saiku#1393.