The context block system provides a structured, manipulable representation of the LLM prompt. Instead of building prompt strings imperatively, the generation pipeline creates discrete blocks that can be found, replaced, removed, reordered, and extended before compilation into final messages.
The generation pipeline works in stages:
buildContextState() → beforeContext hooks → createDefaultBlocks() → applyBlockConfig() → beforeBlocks hooks → compileBlocks() → expandMessagesFragmentTags() → beforeGeneration hooks → streamText()
buildContextState()loads fragments from storage into a typed state object.beforeContexthooks let plugins modify the state (add/remove fragments, change author input).createDefaultBlocks()converts the state into an array ofContextBlockobjects.applyBlockConfig()applies the user's block configuration — custom blocks, content overrides, reordering, and disabling.beforeBlockshooks let plugins manipulate individual blocks (replace instructions, inject sections, reorder).compileBlocks()prepends[@block=id]markers, groups blocks by role, sorts by order, and joins intoContextMessage[].expandMessagesFragmentTags()expands fragment reference tags inside compiled message content.beforeGenerationhooks operate on the final message strings.
interface ContextBlock {
id: string // 'instructions', 'tools', 'prose', etc.
name?: string // optional human-readable name (used in block markers)
role: 'system' | 'user' // which LLM message this block belongs to
content: string // text content (no [@block] marker — added by compileBlocks)
order: number // sort key within role group
source: 'builtin' | string // 'builtin' for core blocks, plugin name for custom
}compileBlocks() automatically prepends a [@block=...] marker to each block's content during compilation. Block content itself should not include the marker. Blocks are separated by blank lines in the compiled output.
Two marker formats are used:
[@block=id]— for blocks wherenameis absent or matchesid(most builtin blocks)[@block=slug src=id]— for named blocks wherenamediffers fromid;slugis a lowercased, dash-separated version of the name
[@block=instructions]
You are a fiction writer continuing an ongoing story...
[@block=tools]
## Available Tools
...
[@block=my-style-guide src=cb-a1b2c3]
Write in present tense, third person limited.
Other marker types used within block content:
[@plugin=name]— plugin-contributed tool descriptions
Block content carries no markers beyond these. Grouped fragment context uses a consistent hierarchy: ## for the block, ### for the fragment type or group, and #### for each full fragment sheet. Headings are separated from the next heading or body by one blank line. Catalog entries are not headings; they are plain `id` | name | desc rows. Full literary renders keep ids off the page, while editing agents use renderFullFragmentSheet for id-bearing #### headings.
createDefaultBlocks() produces these blocks. Empty sections are omitted.
| Block ID | Role | Order | Content |
|---|---|---|---|
instructions |
system | 100 | Writing assistant instructions |
tools |
system | 200 | Tool usage guidance |
system-fragments |
system | 300 | System-placed sticky fragments |
story-info |
user | 100 | Story name + description |
user-fragments |
user | 200 | User-placed sticky fragments |
fragment-recent |
user | 308 | One full-context block with per-type sections for fragments active in recent context |
fragment-catalog |
user | 330 | One compact catalog block with per-type sections for non-full fragments |
summary |
user | 400 | Story summary (omitted if empty) |
chapter-summaries |
user | 410 | Chapter/arc summaries overlapping the prose window |
prose-recent |
user | 500 | Recent prose chain |
author-input |
user | 600 | Author's direction |
Orders leave space around major prompt phases; closely related fragment blocks sit near each other so full context and catalogs stay together.
prose-recent has two intentional render modes:
- Writer/directions prose windows keep prose as continuous manuscript text under
## Recent Prose, followed by## End of Recent Prose. - Editing and analysis agents that need fragment identity render each prose fragment as
### Name (id)followed by the prose body.
Fragments enter context at one of three depths, by relevance and runner authority:
- Full — the entire body is inlined. Used for sticky fragments (always
relevant) and promoted attention candidates — fragments the runner profile
has selected for full context. These render in semantic aggregate blocks such
as
fragment-pinned,fragment-recent,fragment-writer-context, orfragment-candidates, each grouped by type. Writer provenance comes fromwriterContextIds, a type-agnostic signal the writer records on each prose fragment'smeta. - Catalog — one line per fragment (
`id` | name | desc), grouped by type in a summary-index block such asfragment-catalogorfragment-pinned-catalog. Used for non-full fragments and scoped lookup surfaces. - On demand — not in context at all; the agent fetches with
readFragments.
A fragment's full render is name + content (per type's registry.renderContext),
placed as a #### sheet inside an aggregate full-context block. The description
is dropped, since it's the fragment's summary form and would be a redundant
restatement once the body is inlined. (Image/icon keep it — with no content body,
the description is the substance.) The one exception is the analyze agent,
which renders its semantic full-context sheets with the description (and
inline id) because it edits those fields and needs to see them.
See Context Strategy for the full rationale, relevance signals, and the current no-hard-cap testing stance.
Six utility functions are exported from @tealios/errata-plugin-sdk. All are pure and return new arrays:
import {
findBlock,
replaceBlockContent,
removeBlock,
insertBlockBefore,
insertBlockAfter,
reorderBlock,
} from '@tealios/errata-plugin-sdk'
findBlock(blocks, 'instructions') // ContextBlock | undefined
replaceBlockContent(blocks, 'instructions', 'new text') // ContextBlock[]
removeBlock(blocks, 'summary') // ContextBlock[]
insertBlockBefore(blocks, 'prose', newBlock) // ContextBlock[]
insertBlockAfter(blocks, 'prose', newBlock) // ContextBlock[]
reorderBlock(blocks, 'author-input', 450) // ContextBlock[]When insertBlockBefore/insertBlockAfter can't find the target ID, the new block is appended to the end. The order field determines final position during compileBlocks(), not array position.
Plugins can use the beforeBlocks hook to manipulate context blocks. This is the right abstraction for modifying instructions, injecting sections, or changing how author input is presented.
The hook receives a ContextBlock[] and must return a (possibly modified) ContextBlock[]. Use the SDK helpers or standard array operations.
import { definePlugin, replaceBlockContent } from '@tealios/errata-plugin-sdk'
import type { ContextBlock } from '@tealios/errata-plugin-sdk'
export default definePlugin({
manifest: {
name: 'custom-instructions',
version: '1.0.0',
description: 'Replaces default writing instructions',
},
hooks: {
beforeBlocks(blocks: ContextBlock[]): ContextBlock[] {
return replaceBlockContent(blocks, 'instructions',
'You are a poet. Write in iambic pentameter.'
)
},
},
})import { removeBlock, insertBlockAfter } from '@tealios/errata-plugin-sdk'
// in hooks:
hooks: {
beforeBlocks(blocks) {
let result = removeBlock(blocks, 'summary')
result = insertBlockAfter(result, 'prose', {
id: 'narrator-voice',
role: 'user' as const,
content: 'The narrator speaks in second person.',
order: 550,
source: 'my-plugin',
})
return result
},
}When multiple plugins define beforeBlocks, they run in sequence (order determined by plugin registration). Each plugin receives the blocks returned by the previous one.
compileBlocks() produces the final ContextMessage[]:
- Separates blocks into
systemandusergroups byrole. - Sorts each group by
order(stable sort). - Prepends
[@block=...]marker to each block's content (see Source Markers). - Joins rendered blocks with
\n\n(blank line separator) within each group. - Returns one message per non-empty role group.
import { compileBlocks } from '@/server/llm/context-builder'
const messages = compileBlocks(blocks)
// [
// { role: 'system', content: '[@block=instructions]\n...\n\n[@block=tools]\n...' },
// { role: 'user', content: '[@block=story-info]\n...\n\n[@block=prose]\n...' },
// ]| Want to... | Use |
|---|---|
| Modify which fragments are loaded | beforeContext (operates on ContextBuildState) |
| Change instructions, inject sections, reorder prompt structure | beforeBlocks (operates on ContextBlock[]) |
| Modify the final message strings | beforeGeneration (operates on ContextMessage[]) |
The current block editor lives inside the Agents panel. It gives users full control over each agent's LLM context structure without writing plugins. Users can disable builtin blocks, override their content, create custom blocks (including dynamic script blocks), and reorder everything via drag-and-drop.
Open it from the sidebar under Management > Agents, then pick an agent such as Writer, Prewriter, Librarian Analyze, or Character Chat. The older story-level Block Editor UI was removed when per-agent configuration became the source of truth. The separate Fragment Order panel is still gated by Settings > Generation > Context > Fragment ordering: Custom.
Every generation request produces a set of default blocks from story data (see Default Blocks above). The generation.writer agent stores a per-agent block configuration that is applied on top of those defaults during every generation. The configuration is applied after createDefaultBlocks() and before plugin beforeBlocks hooks, so plugins always see the user's customizations.
createDefaultBlocks(state) → applyBlockConfig(blocks, config, state) → beforeBlocks hooks → compileBlocks()
The configuration is stored as a JSON file at data/stories/<storyId>/branches/<branchId>/agent-blocks/generation.writer.json.
The block config has three parts:
interface BlockConfig {
customBlocks: CustomBlockDefinition[] // user-created blocks
overrides: Record<string, BlockOverride> // per-block overrides (keyed by block ID)
blockOrder: string[] // all block IDs in desired order
}Custom blocks are user-created content blocks injected into the LLM context alongside builtin blocks.
interface CustomBlockDefinition {
id: string // "cb-a1b2c3" — auto-generated, pattern: cb-{4-12 alphanumeric}
name: string // display name (1-100 chars)
role: 'system' | 'user' // which LLM message this block belongs to
order: number // default sort position (used when blockOrder is empty)
enabled: boolean // can be toggled off without deleting
type: 'simple' | 'script' // evaluation mode
content: string // plain text or JavaScript function body
}There are two types of custom blocks:
Plain text injected as-is into the context. Use these for static instructions, world-building notes, style guides, or any fixed content you want the LLM to see.
Examples:
- A writing style directive: "Write in present tense, third person limited. Keep paragraphs short — 2-3 sentences max."
- World rules: "Magic in this world requires spoken incantations. Silent casting is impossible. All spells have a physical cost proportional to their power."
- Tone guidance: "The tone is darkly comedic. The narrator is unreliable and occasionally breaks the fourth wall."
JavaScript function bodies that execute at generation time with access to the full story context. The content is evaluated as new Function('ctx', content) — write it as a function body that receives a ctx parameter and returns a string.
The ctx object contains:
| Field | Type | Description |
|---|---|---|
ctx.story |
StoryMeta |
Story metadata (name, description, summary, settings) |
ctx.proseFragments |
Fragment[] |
Recent prose fragments included in context |
ctx.stickyGuidelines |
Fragment[] |
Pinned guideline fragments |
ctx.stickyKnowledge |
Fragment[] |
Pinned knowledge fragments |
ctx.stickyCharacters |
Fragment[] |
Pinned character fragments |
ctx.guidelineCatalog |
Fragment[] |
Non-pinned guidelines rendered as catalog rows |
ctx.knowledgeCatalog |
Fragment[] |
Non-pinned knowledge rendered as catalog rows |
ctx.characterCatalog |
Fragment[] |
Non-pinned characters rendered as catalog rows |
ctx.authorInput |
string |
The author's current input/direction |
ctx.getFragment(id) |
async (id: string) => Fragment | null |
Fetch any fragment by ID (async — use await) |
ctx.getFragments(type?) |
async (type?: string) => Fragment[] |
List fragments, optionally filtered by type (async — use await) |
ctx.getFragmentByTag(tag) |
async (tag: string) => Fragment | null |
Fetch the first fragment with a matching tag (async — use await) |
ctx.getFragmentsByTag(tag) |
async (tag: string) => Fragment[] |
Fetch all fragments with a matching tag (async — use await) |
Return value: The function must return a string. Scripts can be async — the runner awaits the result. If it returns a non-string, an empty string, or a whitespace-only string, the block is silently omitted from context. If the script throws an error, the block is included with a [Script error in custom block "name"] placeholder so the user can see something went wrong.
Script examples:
Word count tracker:
const total = ctx.proseFragments.reduce((n, f) => n + f.content.split(/\s+/).length, 0)
return `Current story length: approximately ${total} words.`Dynamic character reminder:
const names = ctx.stickyCharacters.map(c => c.name).join(', ')
if (!names) return ''
return `Active characters in this scene: ${names}. Stay consistent with their established voices and mannerisms.`Conditional pacing note:
const proseCount = ctx.proseFragments.length
if (proseCount < 3) return 'This is early in the story. Focus on establishing setting and character.'
if (proseCount > 15) return 'The story is well underway. Begin moving toward resolution of active conflicts.'
return ''Summary-aware context:
if (!ctx.story.summary) return ''
const words = ctx.story.summary.split(/\s+/).length
return `Story summary (${words} words) is available. Avoid contradicting established events.`Input-aware formatting:
if (ctx.authorInput.toLowerCase().includes('dialogue')) {
return 'The author wants dialogue. Use varied dialogue tags, show character emotion through action beats, and avoid long unbroken speeches.'
}
return ''Overrides modify builtin blocks (or custom blocks) without replacing them entirely. They are keyed by block ID.
interface BlockOverride {
enabled?: boolean // false to exclude from context
order?: number // override sort position
contentMode?: 'override' | 'prepend' | 'append' | null // how to modify content
customContent?: string // the content to use with contentMode
}| Mode | Effect |
|---|---|
null / not set |
No content modification — block uses its default content |
'prepend' |
customContent is inserted before the block's default content, separated by a newline |
'append' |
customContent is inserted after the block's default content, separated by a newline |
'override' |
The block's default content is entirely replaced with customContent |
Content modes are useful for tweaking builtin blocks without fully replacing them. For example:
- Prepend extra rules to
instructions: "IMPORTANT: Never use the word 'suddenly'." - Append a note to
story-info: "This is a noir detective story set in 1940s Chicago." - Override
instructionsentirely with your own system prompt.
blockOrder is a flat array of block IDs representing the desired ordering. When present, blocks are assigned position-based order values (0, 1, 2, ...) matching their position in the array. Blocks not in the array keep their default order values.
Since compileBlocks() sorts system and user blocks independently by order, the block order controls the sequence within each role group. Drag-and-drop in the agent block editor updates this array.
applyBlockConfig() processes the configuration in five steps, in this order:
- Evaluate and insert custom blocks — enabled custom blocks are evaluated (simple: content as-is; script: executed with
ctx) and added to the block list. - Apply content overrides — for each block with a
contentModeoverride, the content is modified (prepend/append/override). - Apply
blockOrder— blocks listed inblockOrderget position-based order values. - Apply individual
orderoverrides — per-blockorderoverrides take final precedence. - Remove disabled blocks — blocks with
enabled: falseoverrides are filtered out.
This order means:
- Content overrides apply to the original block content (not reordered content).
blockOrderdrag-and-drop ordering is the primary ordering mechanism.- Per-block
orderoverrides can fine-tune positions beyond what drag-and-drop provides. - Disabling happens last, so a disabled block's content is never evaluated for overrides.
The old story-level generation block CRUD API was removed. Per-agent block configuration now lives under /api/stories/:storyId/agent-blocks/:agentName (see Agent Block System).
The remaining /api/stories/:storyId/blocks routes are compatibility utilities:
| Method | Path | Description |
|---|---|---|
POST |
/blocks/eval-script |
Evaluates a script block body against current story data and returns { result, error } |
Story-wide config sharing endpoints live at /api/stories/:storyId:
| Method | Path | Description |
|---|---|---|
GET |
/export-configs |
Export all non-empty agent block configs |
POST |
/import-configs |
Import and replace agent block configs from a JSON payload |
Generation-writer block configuration is stored at:
data/stories/<storyId>/branches/<branchId>/agent-blocks/generation.writer.json
The file is created on first write. If it doesn't exist, all functions return an empty default config ({ customBlocks: [], overrides: {}, blockOrder: [] }), meaning no modifications are applied and the default blocks pass through unchanged.
Storage functions for per-agent configs live in src/server/agents/agent-block-storage.ts.
The Agents panel is in the sidebar under Management. Pick an agent to open its block editor. It shows model settings, agent-specific toggles, tool toggles, and a unified list of prompt blocks (builtin + custom), merged and sorted by role then order.
Each row shows:
- Drag handle — drag to reorder (updates
blockOrder) - Name — block ID for builtin blocks, custom name for custom blocks
- Role badge —
system(violet) oruser(blue) - Custom badge — shown for custom blocks
- Enable toggle — green checkmark when enabled, click to toggle
- Expand chevron — click to expand the inline editor
When a builtin block is expanded:
- A content preview shows the first 200 characters of the block's default content.
- A content mode selector lets you choose None / Prepend / Append / Override.
- When a mode is selected, a textarea appears for entering the custom content.
Changes save on blur (unfocus), not on every keystroke.
When a custom block is expanded:
- The block type is shown (simple / script).
- A textarea for editing the block content (saves on blur).
- For script blocks: a live preview panel shows the evaluated output in real time (debounced). Evaluation uses the
POST /blocks/eval-scriptendpoint. A collapsible Fragment Reference panel lists all available fragment IDs for easy copy-paste into scripts. - A Delete button to remove the block.
Click Add Context Block at the bottom of an agent editor to open the creation dialog:
- Name — display name for the block
- Role — system or user (determines which LLM message it goes in)
- Type — simple (plain text) or script (JavaScript with
ctxaccess) - Content — the block content or script body
Block config is applied before plugin beforeBlocks hooks. This means:
- Users can disable a builtin block, and plugins won't see it.
- Users can override builtin content, and plugins will see the modified version.
- Custom blocks are visible to plugins and can be further modified by
beforeBlockshooks. - Plugins can still add, remove, or modify any blocks regardless of user config.
If a plugin and user config both try to modify the same block, the user config runs first (content override), then the plugin hook runs on the result.
If your story doesn't need the LLM to use tools, disable the tools block to save context space.
Use content mode Override on the instructions block to replace the default writing assistant prompt with genre-specific instructions.
Create a script custom block in the user role:
if (ctx.proseFragments.length === 0) return ''
const last = ctx.proseFragments[ctx.proseFragments.length - 1]
return `Previously: ${last.content.slice(0, 200)}...`Create a simple custom block in the user role, then drag it between story-info and user-fragments in the block list.
Create a script block that checks story data and returns content only when relevant:
const hasCharacters = ctx.stickyCharacters.length > 0
if (!hasCharacters) return 'No characters have been defined yet. Introduce new characters naturally.'
return ''| File | Purpose |
|---|---|
src/server/blocks/schema.ts |
Zod schemas for BlockOverride, CustomBlockDefinition, BlockConfig |
src/server/agents/agent-block-storage.ts |
File-based CRUD for per-agent block config |
src/server/blocks/apply.ts |
applyBlockConfig() — evaluates custom blocks, applies overrides/ordering/disabling. Script blocks receive a generic context object (not tied to ContextBuildState). |
src/server/routes/agent-blocks.ts |
API routes under /stories/:storyId/agent-blocks/* |
src/lib/api/blocks.ts |
Legacy compatibility client for script eval and config bundles |
src/lib/api/agent-blocks.ts |
Per-agent block API client |
src/lib/api/types.ts |
TypeScript types (BlockConfig, CustomBlockDefinition, BlockOverride, etc.) |
src/components/agents/AgentConfigurePanel.tsx |
Main per-agent block editor component |
src/components/blocks/BlockCreateDialog.tsx |
Custom block creation dialog |
src/components/blocks/BlockContentView.tsx |
Context preview renderer |
tests/agents/agent-block-storage.test.ts |
Storage CRUD tests |
tests/blocks/apply.test.ts |
Config application logic tests |
tests/api/blocks-routes.test.ts |
Compatibility route tests |
The agent block system extends the same block-based context approach to non-generation agents (librarian, character chat). Each agent registers block definitions that describe how to assemble its system prompt and user context from story data. Users can customize agent prompts through the same override mechanism used for generation blocks.
Agent blocks follow the same lifecycle as generation blocks, but for agent invocations:
AgentBlockContext → createDefaultBlocks() → applyBlockConfig() → compileBlocks() → agent.stream()
- The calling agent builds an
AgentBlockContextwith relevant story data. compileAgentContext()looks up the agent's registered block definitions, creates default blocks, applies per-story config overrides, and compiles into messages.- The compiled system/user messages and filtered tools are passed to the agent's
ToolLoopAgentconstruction (viacreateStreamingRunnerfor factory-based agents, or the agent's own runner for bespoke ones).
A superset context object that agents populate with the fields they need:
interface AgentBlockContext {
story: StoryMeta
proseFragments: Fragment[]
stickyGuidelines: Fragment[]
stickyKnowledge: Fragment[]
stickyCharacters: Fragment[]
guidelineCatalog: Fragment[]
knowledgeCatalog: Fragment[]
characterCatalog: Fragment[]
systemPromptFragments: Fragment[]
// Agent-specific fields (used by block builders that need them):
allCharacters?: Fragment[]
allKnowledge?: Fragment[]
newProse?: { id: string; content: string }
character?: Fragment
personaDescription?: string
targetFragment?: Fragment
instructions?: string
operation?: string
guidance?: string
selectedText?: string
sourceContent?: string
contextBefore?: string
contextAfter?: string
pluginToolDescriptions?: Array<{ name: string; description: string }>
}Registered via agentBlockRegistry.register():
interface AgentBlockDefinition {
agentName: string
displayName: string
description: string
createDefaultBlocks: (ctx: AgentBlockContext) => ContextBlock[]
availableTools: string[]
buildPreviewContext?: (dataDir: string, storyId: string) => Promise<AgentBlockContext>
}Agent names double as model role keys. The system derives a fallback chain from each dot-separated key and walks it until a configured provider is found.
Algorithm: split the key on ., pop the last segment, repeat. Append generation if not already terminal.
librarian.chat → ['librarian.chat', 'librarian', 'generation']
generation.prewriter → ['generation.prewriter', 'generation']
character-chat.chat → ['character-chat.chat', 'character-chat', 'generation']
directions.suggest → ['directions.suggest', 'directions', 'generation']
generation → ['generation']
getModel(dataDir, storyId, { role }) in src/server/llm/client.ts resolves a model by walking the fallback chain (agents call it through resolveAgentRuntime(), which bundles the story-level disableThinking/generationLimits knobs into the same resolution):
- Story
modelOverridesmap — For each key in the chain, checkstory.settings.modelOverrides[key]for aproviderId/modelIdpair. - Legacy field map — If no override matched, check legacy per-field story settings (e.g.
librarianProviderId,characterChatModelId). See backward compatibility below. - Global default — If the story has no match, fall back to
globalConfig.defaultProviderId. - Error — If nothing is configured, throw with a descriptive message.
The first non-null providerId found wins. If the match includes a modelId, that model is used; otherwise the provider's defaultModel is used.
Two compatibility layers normalize old configuration formats:
OVERRIDE_KEY_ALIASES — Normalizes old camelCase keys in modelOverrides to dot-separated agent names:
| Old key | Normalized to |
|---|---|
characterChat |
character-chat.chat |
librarianChat |
librarian.chat |
librarianRefine |
librarian.refine |
proseTransform |
librarian.prose-transform |
prewriter |
generation.prewriter |
If both old and new key forms exist, the new-style key takes priority.
LEGACY_FIELD_MAP — Maps fallback chain keys to old per-field story JSON property names:
| Key | providerId field |
modelId field |
|---|---|---|
generation |
providerId |
modelId |
librarian |
librarianProviderId |
librarianModelId |
character-chat |
characterChatProviderId |
characterChatModelId |
librarian.prose-transform |
proseTransformProviderId |
proseTransformModelId |
librarian.chat |
librarianChatProviderId |
librarianChatModelId |
librarian.refine |
librarianRefineProviderId |
librarianRefineModelId |
directions |
directionsProviderId |
directionsModelId |
The Settings panel exposes four namespace-level roles for model selection. These serve as defaults for all agents within that namespace:
| Key | Label | Description |
|---|---|---|
generation |
Generation | Main prose writing |
librarian |
Librarian | Background analysis and summaries |
character-chat |
Character Chat | In-character conversations |
directions |
Directions | Story direction suggestions |
Per-agent overrides are configured in the Agent Context panel's block editor. For example, setting a model on generation.prewriter overrides only the prewriter, while the writer inherits from generation. An agent with no per-agent override walks up to its namespace, then to generation, then to the global default.
| Agent Name | Display Name | Description |
|---|---|---|
generation.writer |
Writer | Main prose generation agent |
generation.prewriter |
Prewriter | Writing planner that produces briefs for the writer |
librarian.analyze |
Librarian Analyze | Analyzes prose fragments for continuity signals |
librarian.chat |
Librarian Chat | Conversational assistant with write-enabled tools |
librarian.refine |
Librarian Refine | Refines non-prose fragments using story context |
librarian.optimize-character |
Librarian Optimize Character | Optimizes character sheets using depth-focused writing methodology |
librarian.prose-transform |
Prose Transform | Transforms selected prose spans |
character-chat.chat |
Character Chat | In-character conversation |
directions.suggest |
Directions | Suggests possible story directions based on current context |
Agent block configs are stored at:
data/stories/<storyId>/branches/<branchId>/agent-blocks/<agentName>.json
Each config file follows the same BlockConfig schema (custom blocks, overrides, block order) plus:
disabledTools: string[]— filters which tools the agent can use; default block builders also receivectx.disabledToolsand the post-filterctx.enabledToolsso generated instructions can avoid naming unavailable toolsdisableAutoAnalysis?: boolean— currently used bylibrarian.analyzeto suppress automatic post-generation analysis
All endpoints are under /api/stories/:storyId/agent-blocks.
| Method | Path | Description |
|---|---|---|
GET |
/agent-blocks |
List all registered agents with their block definitions |
GET |
/agent-blocks/:agentName/export-config |
Export one agent's config in a shareable JSON envelope |
POST |
/agent-blocks/:agentName/import-config |
Import and replace one agent's config |
GET |
/agent-blocks/:agentName |
Get agent block config, builtin blocks, and available tools |
GET |
/agent-blocks/:agentName/preview |
Compile and preview the agent's full context |
PATCH |
/agent-blocks/:agentName/config |
Update agent block config (overrides, blockOrder, disabledTools, disableAutoAnalysis) |
POST |
/agent-blocks/:agentName/custom |
Create a custom block for an agent |
PUT |
/agent-blocks/:agentName/custom/:blockId |
Update a custom block |
DELETE |
/agent-blocks/:agentName/custom/:blockId |
Delete a custom block |
The Agent Configure panel is accessible from the sidebar under Management > Agents. It allows browsing registered agents, viewing their compiled context, exporting/importing configs, changing provider/model/temperature per agent, toggling tools, and customizing block overrides.
| File | Purpose |
|---|---|
src/server/agents/agent-block-context.ts |
AgentBlockContext type definition |
src/server/agents/agent-block-registry.ts |
Agent block definition registry |
src/server/agents/agent-block-storage.ts |
Per-agent block config storage |
src/server/agents/compile-agent-context.ts |
compileAgentContext() — assembles messages from blocks |
src/server/agents/create-streaming-runner.ts |
createStreamingRunner() — standard pipeline factory (constructs the ToolLoopAgent) |
src/server/agents/drain-agent-stream.ts |
drainAgentStream() — shared fullStream → AgentStreamEvent translator |
src/server/agents/create-event-stream.ts |
createEventStream() — shared NDJSON stream builder |
src/server/agents/stream-types.ts |
AgentStreamEvent, AgentStreamResult, ChatResult types |
src/server/librarian/blocks.ts |
Block definitions for all librarian agents |
src/server/character-chat/blocks.ts |
Block definitions for character chat |
src/server/routes/agent-blocks.ts |
API routes |
src/lib/api/agent-blocks.ts |
Frontend API client |
src/components/agents/AgentConfigurePanel.tsx |
UI panel |
tests/agents/agent-block-storage.test.ts |
Storage tests |
tests/agents/agent-blocks.test.ts |
Block registration tests |
tests/agents/compile-agent-context.test.ts |
Context compilation tests |