Context & State
Context
Context is an optional first argument of a handler function. It provides request metadata and response control.
Reading metadata
- AssemblyScript
- Go
- Rust
export function myMethod(ctx: Context, input: string): string {
const rid = ctx.requestId;
const tid = ctx.traceId;
const sid = ctx.sessionId;
const me = ctx.skillId;
const who = ctx.callerId;
const mtd = ctx.method;
// ...
}
func MyMethod(ctx *sdk.Context, input string) (string, error) {
rid := ctx.RequestID()
tid := ctx.TraceID()
sid := ctx.SessionID()
lid := ctx.LLMSessionID()
me := ctx.SkillID()
who := ctx.CallerID()
mtd := ctx.Method()
// ...
}
pub fn my_method(ctx: &Context, input: String) -> Result<String, SkillError> {
let rid = ctx.request_id();
let tid = ctx.trace_id();
let sid = ctx.session_id();
let lid = ctx.llm_session_id();
let me = ctx.skill_id();
let who = ctx.caller_id();
let mtd = ctx.method();
// ...
}
LLM context
When a skill is invoked as an MCP tool, the host returns the result to the LLM. LLM context lets you prepend additional instructions or hints that the LLM sees before the tool output. The final response the LLM receives looks like:
<llm context lines>
---
<tool result>
This is useful for guiding the LLM's interpretation of the result — for example, warning about binary content, suggesting next steps, or adding formatting hints.
setLLMContext(text)— sets the LLM context (replaces any previous value).appendLLMContext(text)— appends a line to the existing LLM context. Call multiple times to build up multi-line context.
- AssemblyScript
- Go
- Rust
ctx.setLLMContext("Warning: file contains binary data");
ctx.appendLLMContext("Tip: use --format json");
ctx.SetLLMContext("Warning: file contains binary data")
ctx.AppendLLMContext("Tip: use --format json")
ctx.set_llm_context("Warning: file contains binary data");
ctx.append_llm_context("Tip: use --format json");
Persistent state (Go, Rust)
Each skill can store state between invocations. State is passed in the request and returned in the response.
note
State helpers are currently available in Go and Rust SDKs only.
Save and load
- Go
- Rust
type MyState struct {
Counter int `msgpack:"counter"`
LastInput string `msgpack:"last_input"`
}
func Process(ctx *sdk.Context, input string) (string, error) {
var state MyState
version, _ := sdk.UnmarshalState(ctx.Request().State, &state)
state.Counter++
state.LastInput = input
stateBytes, _ := sdk.MarshalState(1, state)
resp := ctx.Response()
resp.State = stateBytes
return fmt.Sprintf("Call #%d", state.Counter), nil
}
#[derive(Serialize, Deserialize, Default)]
struct MyState {
counter: i32,
last_input: String,
}
fn process(ctx: &mut Context, input: String) -> Result<String, SkillError> {
let (version, mut state) = unmarshal_state::<MyState>(&ctx.request().state)?;
state.counter += 1;
state.last_input = input;
let state_bytes = marshal_state(1, &state)?;
ctx.response().state = state_bytes;
Ok(format!("Call #{}", state.counter))
}
State versioning
When the state structure changes, use the version number for migration:
- Go
- Rust
var state MyState
version, _ := sdk.UnmarshalState(req.State, &state)
switch version {
case 0:
// First run, no state
case 1:
// Old version — migrate
state.NewField = "default"
case 2:
// Current version
}
stateBytes, _ := sdk.MarshalState(2, state)
let (version, mut state) = unmarshal_state::<MyState>(&req.state)?;
match version {
0 => { /* First run */ }
1 => { state.new_field = "default".into(); }
2 => { /* Current */ }
_ => {}
}
let state_bytes = marshal_state(2, &state)?;
Notes
- State is stored on the host side and persists across restarts
- State size is limited (recommended up to 1 MB)
- Empty state is normal for the first invocation
- Each skill can only access its own state