Skill Composer
Status: Design landing Reference epics: INK-835, INK-829 ADRs: ADR-016
The Skill Composer is the subagent that turns a skill package plus a concrete invocation request into a prompt and a subgraph shape the main graph can run. It is the only mechanism by which a skill becomes a runnable turn. There is no compiled artifact, no pre-rendered prompt cache, no separately trained module.
This page defines the Composer’s algorithm and its I/O contract. The surrounding framing — what a skill is, why it is not world content, where it lives — is in Skill System. The entity shape, schema, and sync are in Skill Storage.
Where the Composer lives
Section titled “Where the Composer lives”The Composer is a LangGraph subagent in the Python sidecar (see Agent Core System). It is dispatched from the main agent graph using the same mechanism as any other subagent dispatch — a subgraph call on the same thread_id, sharing state, memory, and the checkpointer with the parent turn.
The Composer is authored — its system prompt and control flow live in the sidecar as ordinary code. The Composer does not compose itself; it is not recursively produced from a skill package.
The retired Rust-side “skill composer” — a ProcessType::SkillComposer process in the agent-harness — is not part of this design. The process model is gone with the LangGraph migration; the Composer concept survives in the sidecar.
What the Composer is not
Section titled “What the Composer is not”- Not a compiler. It produces an ephemeral plan for one invocation, not a durable artifact.
- Not a separate runtime. It runs inside the sidecar’s LangGraph graph like any other subagent.
- Not a tool router. The main graph’s planner decides which skill to apply; the Composer takes that decision as input.
- Not a permission arbiter. The Composer is bounded by the caller’s capability set; it does not grant or check permissions itself. Permissions and enable/disable state are gated by the planner before dispatch (see Skill Storage).
- Not stateful across invocations. Each Composer call is independent. Previous composed subgraphs are not retained and not consulted.
Inputs
Section titled “Inputs”The main graph dispatches the Composer with a request object. The shape (informal schema):
ComposeRequest { skill_package: SkillPackage, // the skill to compose for user_request: String, // verbatim user text / scheduled-task prompt invocation_source: InvocationSource,// explicit | agent-selected | scheduled channel_id: Option<ChannelId>, // None for workspace-scope invocations thread_id: ThreadId, // shared with parent turn memory_context: MemoryContext, // prefetched relevant items from the four tiers available_tools: Vec<ToolDescriptor>,// intersection of skill.tool_surface and caller capabilities caller_capabilities: CapabilitySet, // authoritative; the Composer reads, does not widen parameters: Map<String, Value>, // invocation parameters matched against skill's parameter schema}A few properties are worth naming explicitly.
memory_contextis prefetched, not a handle. The parent turn’s context node has already assembled the four-tier reads (see Agent Memory System). The Composer receives the selected items, not a memory API. It may mark which items its produced prompt depends on — the main graph uses that list when replaying the thread.available_toolsis already intersected. The caller’s capability resolver has already narrowedskill.tool_surfaceagainst the caller’s capabilities. The Composer may not reference tools outsideavailable_toolsin its output.parametersis typed by the skill. The parent graph has already validated the invocation parameters against the skill package’s parameter schema (declared in metadata). Missing or mismatched parameters surface as a dispatch error before the Composer is called.channel_idbeingNonemeans workspace-scope. The Composer does not derive scope; it reads it.
Outputs
Section titled “Outputs”The Composer returns a composition result:
Composition { prompt: ComposedPrompt, // concrete prompt for the turn subgraph: SubgraphShape, // tool availability, sequencing hints, interrupt points, exit condition used_artifacts: Vec<ArtifactRef>, // which skill artifacts the prompt includes expected_tool_calls: Vec<ToolName>, // best-effort sequence hint (not enforced) declared_interrupts: Vec<InterruptName>, // named interrupt points the subgraph may fire exit_condition: ExitCondition, // what "done" looks like for this invocation model_preference: Option<ModelVariant>, // carried through from skill metadata}promptis the concrete prompt text. Framing rendered from the skill’sframingfield, augmented with anyDescriptionartifacts the Composer decided to include, followed byExampleartifacts rendered as few-shots when present, followed by the current user request framed byparameters.subgraph.tool_availabilityisavailable_toolsor a subset. The Composer may narrow further (e.g., “this invocation does not need the write tool”) but cannot expand.declared_interruptsare named interrupt points the skill’s contract declares — places where the subgraph may pause for author input (see Agent Core System for the interrupt mechanism). The Composer translates skill-contract interrupt names into concrete graph-node references.exit_conditionis one of: producedoutput_schema-matching output, tool-call budget exhausted, interrupt fired, or caller-cancelled.used_artifactsis the exact list of artifacts the Composer read from the skill package when producing the prompt. The main graph records this on the invocation record so replay is deterministic when the skill has not changed.
Algorithm
Section titled “Algorithm”The Composer runs a small, deterministic algorithm. It is not a long reasoning loop; it is a template-assembly step with a few decision points.
- Validate the package. Fail closed if the skill is missing required fields (name, framing), if the parameter schema is malformed, or if
available_toolsis empty when the skill requires tool use. - Render the framing. Load the skill’s
framingfield. If the framing is aPromptTemplateartifact (authored as MiniJinja), render it againstparameters. Otherwise treat it as literal prose. - Select supporting artifacts. Walk the skill’s artifacts.
Descriptionartifacts whoseinclude_whencondition matches the invocation (scope, parameters, or unconditional) are added to the prompt body.Exampleartifacts are selected by relevance — anExamplecan declare tags; the Composer picks matching examples up to a budget (default: 3). - Assemble the prompt. Concatenate: framing → selected descriptions → few-shot examples → current-invocation framing (which wraps
user_requestandparameters). Section headers and spacing follow a fixed template; the Composer does not reword the authored prose. - Shape the subgraph. Decide tool availability (default: the full
available_toolsset; narrow if the skill’sApproachartifact declares a subset for this invocation’s parameters). Decide whether the subgraph is a single planner-executor loop or a multi-step pipeline (skills with a declaredApproachartifact that describes ordered stages get a staged subgraph). - Declare interrupts. Translate the skill contract’s named interrupt points into concrete graph-node references. A skill that declares an
on_ambiguous_inputinterrupt produces a subgraph whose input-validation node fires that interrupt when its condition holds. - Set exit condition. From the skill contract: either an
output_schemamatch, a bounded tool-call count, or caller-supplied cancellation. - Record used artifacts. Emit
used_artifactswith the exact artifact ids and versions consulted. - Return the composition.
The algorithm does not consult a model for steps 1–8 when the skill’s framing and Approach are authored prose. When the skill’s framing is a PromptTemplate that requires rendering against structured parameters, step 2 uses the sidecar’s MiniJinja implementation; that is a deterministic render, not a model call. The Composer does not call a model to “rewrite” authored prose.
The one path in which the Composer calls a model is step 5 when the skill explicitly declares planner_delegates_shape = true — a skill can opt into asking a model to decide subgraph shape from its Approach narrative. That is a skill-authored choice, not the default.
The thread_id contract
Section titled “The thread_id contract”The composed subgraph runs on the same thread_id as the parent turn. This is intentional:
- The checkpointer records the parent turn and the subgraph as one thread, making replay and time-travel meaningful.
- Memory reads and writes during the subgraph are scoped the same way they would be for any other subagent (see Agent Memory System).
- Interrupts fired inside the subgraph surface to the author with the parent turn’s context, not as a standalone “a skill interrupted” surface.
A sub-skill dispatched from inside a skill’s subgraph is also on the same thread_id. The entire tree of skill invocations for one user turn lives in one thread.
Capability bounding
Section titled “Capability bounding”The Composer never widens capabilities. Concretely:
available_toolsis the hard ceiling. The composition’ssubgraph.tool_availabilityis a subset.- The composed prompt does not grant the model authority the caller lacks. A skill that says “use any tool you need” in its framing still runs inside the caller’s
available_tools. - A nested sub-skill invocation’s
available_toolsis the intersection of the sub-skill’stool_surfaceand the top-level caller’scaller_capabilities— not the parent skill’s declared surface. The surface declares intent; the capability check is always against the caller.
This bounding is enforced by the main graph when it calls the Composer (input construction) and again when it dispatches the composed subgraph (tool-availability check at tool-call time). The Composer is never the sole enforcer.
The Composer’s own tool surface (via MCP)
Section titled “The Composer’s own tool surface (via MCP)”When the Composer is dispatched from a conversation whose purpose is creating or editing a skill (an authoring conversation, not an application conversation), it needs to call back into the Rust domain to write the skill. Those writes are exposed over MCP (see MCP System):
create_skill— create a new skill package with metadata.update_skill— update an existing skill’s metadata or framing.list_skills— list workspace skills with filters (source, tags, enabled).add_artifact— add a typed artifact to a skill package.
These tools are exposed from the Rust side over MCP. The sidecar’s Composer calls them like any other MCP tool. They are not available to application-time Composer invocations — a skill running to produce content cannot edit its own definition. The planner only wires these tools into the Composer’s available_tools when the invocation source is an authoring conversation.
The underlying Rust implementation delegates to the application layer’s CRUD use cases against the SkillPackageRepository. The transport (MCP) and the domain layer are cleanly separated.
Errors and failure modes
Section titled “Errors and failure modes”The Composer has a small set of failure modes, all of which return a structured error instead of a composition:
MissingRequiredField— the skill package is missing name, framing, or parameter schema.MalformedTemplate— a MiniJinja render failed.UnknownTool— the skill references a tool by name that is not inavailable_tools. The Composer does not silently drop the reference; it fails the composition so the author can correct either the skill or the caller’s capability set.ParameterMismatch— reserved; parameter validation happens before the Composer runs, but the Composer repeats a lightweight check as a defensive measure.ArtifactBudgetExceeded— the selected artifacts exceed a configured context-budget ceiling. The Composer returns the error with a suggested trimming, rather than arbitrarily dropping content.CapabilityNarrowing— the intersection ofskill.tool_surfaceand caller capabilities is empty for a skill that requires tool use. The planner should usually catch this before dispatch; if the Composer sees it, it fails closed.
Errors surface to the main graph’s planner, which decides how to proceed — typically by surfacing a message to the user and not attempting the skill.
Observability
Section titled “Observability”Each Composer call produces an invocation record stored alongside the conversation’s turn record (not as a separate “execution trace” table — those were retired with the DSPy skill-compilation pipeline). The record contains:
- Skill id and version.
- Invocation source (explicit, agent-selected, scheduled).
used_artifactslist (ids + versions).expected_tool_callssequence hint.- Whether the composition succeeded; error variant if not.
The record is enough to answer “what did the Composer do?” without a separate trace system. Detailed per-artifact observability is a future expansion, not part of this landing.
Relationship to other systems
Section titled “Relationship to other systems”- Skill System — the surrounding framing; why skills are not world content.
- Skill Storage — the entity shape the Composer reads.
- Agent Core System — subagent dispatch, checkpointer, interrupts.
- Agent Memory System — the four tiers whose items are pre-read into
memory_context. - MCP System — the transport for the Composer’s skill-authoring tools.
- LLM System — the Composer’s rare model calls (only when the skill explicitly opts in) go through this system.
- Permission System — capability resolution that produces
available_toolsandcaller_capabilities. - Submit Boundary — writes made by the composed subgraph cross the boundary; the composition itself does not.
What this page does not do
Section titled “What this page does not do”- It does not describe the data model of
SkillPackage,SkillArtifact, orskill_permissions. See Skill Storage. - It does not describe the main graph’s planner logic that decides which skill to apply. See Agent Core System.
- It does not specify a concrete prompt template. The assembly order is fixed; the text within each section is authored, per-skill.
- It does not describe how the Composer is unit-tested. Testability follows ordinary sidecar subagent test patterns.
Was this page helpful?
Thanks for your feedback!