Skip to main content

Sessions and runs API

Sessions are the durable execution containers. Runs are individual execution attempts queued or executed inside those sessions. Channels are separate top-level public conversation resources. When a channel decides that one session should speak publicly, the daemon creates a normal run of kind ChannelDelivery inside that session. Read Channels API for the public conversation surface. Projects are also separate top-level daemon resources. Starting a project task still creates a normal run inside the assigned session rather than a separate project-local executor. Read Projects API for that coordination layer.

Endpoint inventory

Sessions:
  • POST /v1/sessions
  • GET /v1/sessions
  • GET /v1/sessions/{session_id}
  • GET /v1/sessions/{session_id}/memory-context
  • GET /v1/sessions/{session_id}/memory-search
  • GET /v1/sessions/{session_id}/skills
  • POST /v1/sessions/{session_id}/persona
  • PUT /v1/sessions/{session_id}/persona
  • DELETE /v1/sessions/{session_id}/persona
  • POST /v1/sessions/{session_id}/capability-scope
  • PUT /v1/sessions/{session_id}/capability-scope
  • DELETE /v1/sessions/{session_id}/capability-scope
  • POST /v1/sessions/{session_id}/credential-scope
  • PUT /v1/sessions/{session_id}/credential-scope
  • DELETE /v1/sessions/{session_id}/credential-scope
  • GET /v1/sessions/{session_id}/reply-targets
  • POST /v1/sessions/{session_id}/reply-targets
  • PUT /v1/sessions/{session_id}/reply-targets
  • DELETE /v1/sessions/{session_id}/reply-targets
  • POST /v1/sessions/{session_id}/route-policy
  • PUT /v1/sessions/{session_id}/route-policy
  • DELETE /v1/sessions/{session_id}/route-policy
  • GET /v1/sessions/{session_id}/events
  • GET /v1/sessions/{session_id}/stream
  • POST /v1/sessions/{session_id}/input
  • POST /v1/sessions/{session_id}/runs
  • POST /v1/sessions/{session_id}/interrupt
  • POST /v1/sessions/{session_id}/end
Runs:
  • GET /v1/runs
  • GET /v1/runs/{run_id}
  • GET /v1/runs/{run_id}/external-actions
  • GET /v1/runs/{run_id}/events
  • GET /v1/runs/{run_id}/stream
  • POST /v1/runs/{run_id}/cancel
  • GET /v1/runs/{run_id}/debug
  • GET /v1/runs/{run_id}/debug/artifacts/{artifact_id}
Questions and approvals live on separate resume endpoints. Read Questions and approvals API.

Create or reuse a session

POST /v1/sessions accepts:
  • session_id: optional caller-selected identifier
  • thread_id: optional provider-side thread id
  • persona_id: optional persona bound at creation time
  • capability_scope: optional session capability override persisted on the session
  • credential_scope: optional session credential override persisted on the session
Example:
{
  "session_id": "review-demo",
  "persona_id": "reviewer",
  "capability_scope": {
    "skill_deny": ["live-inline-marker"]
  },
  "credential_scope": {
    "route_allow": ["openai"],
    "mcp_server_deny": ["github"]
  }
}
Behavior:
  • The handler currently returns 201 Created.
  • If the requested session_id already exists, Kheish reuses that session.
  • A reuse request is rejected if the supplied persona_id conflicts with the already bound persona.
  • A reuse request is also rejected if the supplied capability_scope conflicts with the already persisted session scope.
  • A reuse request is also rejected if the supplied credential_scope conflicts with the already persisted session scope.
GET /v1/sessions supports one query parameter:
  • persona_id: filter by currently bound persona snapshot

Session-scoped defaults

The daemon lets you persist session-local defaults that future runs inherit unless they are overridden per request.

Route policy

POST and PUT on /v1/sessions/{session_id}/route-policy both persist the same SessionRoutePolicy shape:
{
  "route_policy": {
    "provider": "openrouter",
    "generation": {
      "model": "openai/gpt-5.4-mini",
      "tool_choice": "auto",
      "allow_parallel_tool_calls": true,
      "response_format": {
        "type": "text"
      }
    }
  }
}
Fields:
  • provider: preferred daemon route id
  • generation.model
  • generation.fallback_model
  • generation.tool_choice
  • generation.allow_parallel_tool_calls
  • generation.max_output_tokens
  • generation.temperature
  • generation.response_format
Route precedence is:
  1. explicit run override on SubmitInputRequest
  2. persisted session route_policy
  3. daemon default_route
Named-route rule:
  • On a named-route daemon, the request provider field carries the route id, not necessarily the underlying provider family.
Terminology note:
  • request provider selects a configured route id
  • backend responses and secret records may still use provider to name the underlying provider family

Session capability scope

POST and PUT on /capability-scope both accept:
{
  "capability_scope": {
    "skill_allow": ["research:browser"],
    "skill_deny": ["live-inline-marker"],
    "mcp_server_allow": ["openaiDeveloperDocs"],
    "mcp_tool_deny": ["mcp__openaiDeveloperDocs__fetch_openai_doc"]
  }
}
Supported fields:
  • skill_allow
  • skill_deny
  • mcp_server_allow
  • mcp_server_deny
  • mcp_tool_allow
  • mcp_tool_deny
The daemon normalizes the scope before persistence. The effective runtime scope is:
  1. persona capability baseline, when the session has a bound persona
  2. restricted by the persisted session capability scope

Session credential scope

POST and PUT on /credential-scope both accept:
{
  "credential_scope": {
    "route_allow": ["openai", "anthropic"],
    "connector_allow": ["slack-prod"],
    "connector_credential_allow": ["slack-prod:BOT_TOKEN"],
    "mcp_server_deny": ["github"]
  }
}
Supported fields:
  • route_allow
  • route_deny
  • connector_allow
  • connector_deny
  • connector_credential_allow
  • connector_credential_deny
  • mcp_server_allow
  • mcp_server_deny
This scope does not replace CapabilityScope.
  • CapabilityScope decides what the session can see or call
  • CredentialScope decides which auth-backed routes, connector credentials, and credentialed MCP surfaces can actually resolve
Normalization rules worth remembering:
  • entries are trimmed, sorted, and deduplicated before persistence
  • * collapses an allow-list or deny-list to that wildcard alone
  • if you scope connectors with connector_allow or connector_deny and leave connector_credential_allow empty, concrete connector credential env keys default to denied
Mutation rule:
  • persona, capability-scope, and credential-scope changes are only allowed while the session is idle for topology mutation
  • non-idle credential-scope mutation is returned as 409 Conflict
CLI examples:
./target/debug/kheish-daemon sessions set-credential-scope demo \
  --credential-scope-file scope.json

./target/debug/kheish-daemon sessions set-credential-scope demo --clear

Inspect effective memory and visible skills

Kheish exposes derived session-inspection endpoints that show what the daemon currently considers eligible for the next run before final prompt packing.

Session memory context

GET /v1/sessions/{session_id}/memory-context returns a SessionMemoryContextView. Important fields:
  • session_id
  • effective_capability_scope
  • learning_scopes
  • learned_context
  • recovered_memory
  • visible_skills
This is a derived runtime-facing view, not the canonical session journal. Use it to inspect:
  • which semantic learnings are currently prompt-eligible
  • which recovered run memories are currently eligible for recovery
  • which skills are currently visible to the session
The runtime can still truncate or omit parts of learned_context or recovered_memory later when it packs the final prompt for one specific input and model budget. Current projection rules worth remembering:
  • procedure and run_summary learnings stay out of learned_context
  • learnings with publish_tier=provisional stay out of learned_context
  • learnings with verification_status=failed stay out of learned_context
  • automatically published learnings stay out of learned_context until verification_status=verified
  • learnings with policy_decision=escalated stay out of learned_context
  • promoted procedural skills only appear in visible_skills after their promoted-skill rollout state becomes active
GET /v1/sessions/{session_id}/memory-search returns a bounded SessionMemorySearchView. Supported query parameters:
  • query
  • limit
Current behavior:
  • when query is omitted, the daemon returns a recent browse view
  • when query is present, the daemon lexically ranks visible learnings, recovered runs, and visible skills
  • returned result kinds are learning, recovered_run, and skill
  • skills only appear in the search results when a query is present
  • omitted limit defaults to 12
  • the daemon clamps limit to a maximum of 50
Important distinction:
  • memory-context shows the current eligible automatic projection
  • memory-search shows the broader daemon-owned memory browse/search surface visible to that session
Returned search results include operator-facing metadata such as:
  • source_id
  • title
  • excerpt
  • score
  • timestamp_ms
  • scope
  • prompt_eligible
  • matched_fields

Session-visible skills

GET /v1/sessions/{session_id}/skills returns the skills currently visible to that session. Supported query parameter:
  • query
This endpoint applies the same visibility rules used during input assembly and runtime tool exposure:
  • effective session capability scope
  • promoted-skill source-scope visibility

Session reply targets

Structured session reply-target requests are accepted on:
  • POST /v1/sessions/{session_id}/reply-targets
  • PUT /v1/sessions/{session_id}/reply-targets
Example:
{
  "reply_targets": [
    {
      "type": "telegram",
      "connector": "prod-bot",
      "chat_id": 123456789,
      "message_thread_id": 12
    },
    {
      "type": "http",
      "url": "https://example.com/hooks/kheish",
      "headers": {
        "Authorization": "Bearer reply-token"
      }
    }
  ]
}
Supported request variants:
  • raw
  • telegram
  • slack
  • http
GET /v1/sessions/{session_id}/reply-targets returns a SessionReplyTargetsView envelope:
{
  "reply_targets": [
    {
      "plugin": "telegram",
      "address": "{\"connector\":\"prod-bot\",\"chat_id\":123456789,\"message_thread_id\":12}"
    }
  ]
}
The returned reply_targets array is normalized ReplyHandle data, not the original structured request shape.

Persona binding

Session persona mutation accepts:
{
  "persona_id": "reviewer"
}
These endpoints mutate the bound session snapshot, not the underlying persona record:
  • POST /v1/sessions/{session_id}/persona
  • PUT /v1/sessions/{session_id}/persona
  • DELETE /v1/sessions/{session_id}/persona
Persona and capability-scope changes are only allowed while the session is idle for topology mutation.

Submit work

Kheish intentionally exposes two session submission modes:
  • POST /v1/sessions/{session_id}/input
    • executes inline and returns an updated SessionView
  • POST /v1/sessions/{session_id}/runs
    • queues detached work and returns 202 Accepted with a RunView
Use /input when the client wants the session view after execution. Use /runs when the client wants an explicit run handle for later polling or streaming.

SubmitInputRequest

Request fields:
  • provider: optional run-scoped route override
  • source_plugin
  • source_kind
  • actor_id
  • content
  • input_items
  • attachments
  • generation
  • completion_requirements
  • metadata
  • binding_keys
  • reply_targets
  • reply_plugin
  • reply_address
Important rules:
  • input_items cannot be combined with compatibility content or attachments
  • when you submit only input_items, send "content": "" so the request still matches the current SubmitInputRequest shape
  • content, attachments, or input_items must provide actual input
  • binding_keys are durable session-affinity keys remembered by the daemon
  • one binding key cannot later be rebound to a different session
  • reply_plugin and reply_address are compatibility fields; prefer structured reply_targets
Attachment inputs use the same daemon-owned asset model documented in Assets API.

Content plus attachments example

{
  "provider": "openrouter",
  "generation": {
    "model": "openai/gpt-5.4-mini",
    "tool_choice": "auto",
    "allow_parallel_tool_calls": true,
    "max_output_tokens": 4000,
    "response_format": {
      "type": "text"
    }
  },
  "content": "Review the attached specification and produce a concise summary.",
  "attachments": [
    {
      "type": "asset_reference",
      "asset_id": "asset-123"
    }
  ],
  "binding_keys": ["team:docs"],
  "completion_requirements": [
    {
      "type": "workspace_file",
      "path": "reports/spec-summary.md"
    }
  ]
}

Ordered multimodal input example

{
  "provider": "openrouter",
  "content": "",
  "generation": {
    "model": "openai/gpt-5.4-mini",
    "tool_choice": "auto",
    "allow_parallel_tool_calls": true,
    "response_format": {
      "type": "text"
    }
  },
  "input_items": [
    {
      "type": "text",
      "text": "Compare this screenshot with the note that follows."
    },
    {
      "type": "inline_asset",
      "file_name": "screen.png",
      "media_type": "image/png",
      "content_base64": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAAB..."
    },
    {
      "type": "asset_reference",
      "asset_id": "asset-note-1"
    }
  ]
}

Completion requirements

Current daemon-enforced completion requirements are explicit and opt-in. Supported shape today:
[
  {
    "type": "workspace_file",
    "path": "reports/final.md"
  }
]

Inspect session state

GET /v1/sessions/{session_id} returns a SessionView containing:
  • session_id
  • agent_id
  • snapshot
  • route_policy
  • capability_scope
  • effective_capability_scope
  • credential_scope
  • effective_credential_scope
  • persona
  • reply_targets
  • outputs
GET /v1/sessions/{session_id}/events returns SessionEventLogView:
  • session
  • daemon_outputs
GET /v1/sessions/{session_id}/stream exposes the session SSE stream:
curl -N http://127.0.0.1:4000/v1/sessions/demo/stream

Session interrupt and end

POST /v1/sessions/{session_id}/interrupt returns:
  • interrupted
  • snapshot
POST /v1/sessions/{session_id}/end accepts:
{
  "reason": "done"
}

Run inspection

GET /v1/runs supports one query parameter:
  • session_id
RunView includes:
  • run_id
  • session_id
  • agent_id
  • kind
  • status
  • submitted_at_ms
  • updated_at_ms
  • started_at_ms
  • finished_at_ms
  • queued_position
  • request
  • pending_approval_ids
  • pending_question_ids
  • pending_questions
  • outputs
  • error
Use GET /v1/runs/{run_id} as the source of truth for the route and model actually used by one execution.

Run external actions

GET /v1/runs/{run_id}/external-actions returns the signed external-action audit records attached to that run. Important fields include:
  • action_id
  • timestamp_ms
  • session_id
  • agent_id
  • run_id
  • tool_call_id
  • principal_id
  • parent_principal_id
  • grant_id
  • phase
  • kind
  • target
  • request_digest
  • response_digest
  • outcome
  • prev_hash
  • record_hash
  • signature_alg
  • key_id
  • signature
Use this endpoint when you need the durable operator audit for outbound calls or side effects without opening the full debug bundle.

Run debug surfaces

GET /v1/runs/{run_id}/debug returns one RunDebugView:
  • run_id
  • level
  • artifacts
GET /v1/runs/{run_id}/debug/artifacts/{artifact_id} returns raw UTF-8 text, not a JSON envelope. Useful artifact ids usually include request, provider event stream, and normalized response records such as:
  • turn-0001-attempt-0001-model-request
  • turn-0001-attempt-0001-provider-request
  • turn-0001-attempt-0001-provider-events
  • turn-0001-attempt-0001-model-response

Run events and cancellation

GET /v1/runs/{run_id}/events returns the persisted RunEventEntry[] list, not the SSE envelope format. GET /v1/runs/{run_id}/stream exposes the run SSE stream:
curl -N http://127.0.0.1:4000/v1/runs/run-1/stream
POST /v1/runs/{run_id}/cancel returns the updated RunView.