Context #
Signet currently runs as a cognitive maintenance layer on top of external runtimes — OpenClaw, Claude Code, OpenCode. These are first-class harness integrations and remain so. The problem is that Signet’s capability growth is gated on what each harness exposes: if a harness deprecates a plugin interface, changes its hook model, or simply doesn’t support a new lifecycle event, Signet loses the surface.
The deeper issue is definitional. Every harness integration has to answer the question “what does a Signet harness need to implement?” — right now there is no canonical answer. Each connector is a bespoke translation layer.
The solution is a daemon-owned adapter contract that defines what it means to run a harness on Signet. When the daemon adds a new lifecycle capability, each harness adapter has a clear spec to implement against. The daemon API is the source of truth for the integration contract.
Key constraints:
- All actions in the runtime are Daemon API calls first. The runtime is a thin orchestration layer over the daemon HTTP API, not a parallel implementation of daemon functionality.
- Harnesses are thin clients. They translate platform-specific events into Daemon API calls using the same interfaces the reference runtime uses.
- SDKs for TypeScript, Rust, and Python continue to be first-class. The runtime expands what the SDKs expose, not replaces them.
What the Runtime Is #
The Signet runtime is a session execution loop that:
- Assembles context (memory injection, system prompt, identity) via daemon
- Sends a turn to a configured LLM provider
- Dispatches tool calls through a registered tool registry
- Manages the session lifecycle (start, prompt, end, compaction)
- Records behavioral signals back to the daemon (FTS hits, continuity)
Every one of those steps is a daemon API call or a thin wrapper around one. The runtime doesn’t store state — the daemon does.
The runtime’s only owned concern is the execution loop: taking a user message, assembling what the agent needs to respond, calling the model, handling tools, and returning output. Everything else is delegated.
Core Interfaces #
These interfaces define the integration contract. Implementing them is what it means to be a Signet harness.
Provider
interface Provider {
id: string
complete(messages: Message[], opts?: CompletionOptions): Promise<CompletionResult>
stream(messages: Message[], opts?: CompletionOptions): AsyncIterable<CompletionChunk>
available(): Promise<boolean>
}
interface CompletionOptions {
model?: string
maxTokens?: number
temperature?: number
tools?: ToolDefinition[]
systemPrompt?: string
}
interface CompletionResult {
content: string
toolCalls?: ToolCall[]
stopReason: 'end_turn' | 'tool_use' | 'max_tokens' | 'stop_sequence'
usage: { inputTokens: number; outputTokens: number }
}
Implementations: Anthropic, OpenAI, OpenAI-compatible (Ollama, local). The runtime ships a default Anthropic provider. Additional providers are registered at startup or loaded from config.
Tool
interface Tool {
name: string
description: string
inputSchema: JsonSchema
runBeforeGeneration?: boolean // opt-in to pre-generation research phase
execute(input: unknown, context: ToolContext): Promise<unknown>
}
interface ToolContext {
sessionKey: string
project: string
daemonUrl: string // all daemon calls go through context, never direct
}
Tools are registered in a ToolRegistry
. The runtime dispatches tool calls
from model responses through the registry. Built-in tools: memory_search
,
memory_store
, memory_get
, memory_modify
, memory_forget
— thin wrappers over daemon API endpoints, identical to what the MCP server exposes.
Channel
interface Channel {
kind: string
receive(): Promise<UserTurn>
send(output: AgentOutput): Promise<void>
onClose(handler: () => void): void
}
interface UserTurn {
content: string
attachments?: Attachment[]
metadata?: Record<string, unknown>
}
interface AgentOutput {
content: string
toolResults?: ToolResult[]
streaming?: boolean
}
First channel: CLI (stdin/stdout). Subsequent: HTTP (for harness adapter attachment). Channel is the only interface that varies between the reference runtime and a harness adapter — everything else (provider, tools, lifecycle) is identical.
RuntimeAdapter
This is the interface a harness implements to integrate with Signet. It maps platform-specific lifecycle events to daemon API calls.
interface RuntimeAdapter {
harness: string // 'openclaw' | 'claude-code' | 'opencode' | ...
onSessionStart(params: SessionStartParams): Promise<SessionStartResult>
onUserPromptSubmit(params: PromptSubmitParams): Promise<PromptSubmitResult>
onSessionEnd(params: SessionEndParams): Promise<void>
onPreCompaction(params: PreCompactionParams): Promise<PreCompactionResult>
onCompactionComplete(params: CompactionCompleteParams): Promise<void>
}
All RuntimeAdapter
implementations are thin clients: every method body is a daemon API call. OpenClaw’s adapter implements this interface by calling the same daemon endpoints via its plugin system; other adapters translate their own lifecycle hooks into the same calls.
A harness adapter is a RuntimeAdapter
implementation. Nothing more.
Package Structure #
Signet does not maintain a monorepo-owned native runtime package. Runtime
coverage is provided by connector and plugin packages under integrations/*
, with daemon endpoints as the shared contract.
Session Lifecycle #
Each turn in the execution loop:
1. Channel.receive()
→ get user message
2. executor.preGeneration() [research phase]
→ query daemon for relevant memories (FTS + vector)
→ execute tools with runBeforeGeneration: true
→ results available to model before generation starts
3. context.assemble()
→ system prompt: identity + SOUL.md content + injected memories
→ conversation history
→ pre-generation tool results
4. Provider.complete()
→ call the model with assembled context
5. executor.dispatch() [tool loop]
→ for each tool call in response: ToolRegistry.execute()
→ append results to messages
→ loop back to Provider.complete() until stop_reason = end_turn
6. daemon POST /api/hooks/user-prompt-submit
→ record FTS hits for behavioral signals (predictive scorer training)
7. Channel.send()
→ deliver output to user
Session start:
daemon POST /api/hooks/session-start
→ memories injected into first-turn system prompt
→ continuity checkpoint loaded if session resumed
Session end:
daemon POST /api/hooks/session-end
→ triggers continuity scoring job
→ memory extraction pipeline enqueued
→ session checkpoint saved
The pre-generation research phase (step 2) is the key architectural
difference from a naive chat loop. Tools that declare runBeforeGeneration: true
execute before the model sees the user message. The model gets grounded context instead of generating then correcting.
Daemon API Dependency Map #
Every runtime action maps to a daemon endpoint. This table defines the integration surface a harness adapter must cover for full parity:
| Runtime Action | Daemon Endpoint | Phase |
|---|---|---|
| Session start context | POST /api/hooks/session-start | start |
| Per-prompt context + FTS | POST /api/hooks/user-prompt-submit | turn |
| Session end / extraction | POST /api/hooks/session-end | end |
| Pre-compaction summary | POST /api/hooks/pre-compaction | cmpct |
| Save compaction result | POST /api/hooks/compaction-complete | cmpct |
| Memory search (tool) | POST /api/memory/recall | any |
| Memory store (tool) | POST /api/hooks/remember | any |
| Memory get (tool) | GET /api/memory/:id | any |
| Memory modify (tool) | POST /api/memory/modify | any |
| Memory forget (tool) | POST /api/memory/forget | any |
| Secret injection | POST /api/secrets/exec | tool |
| Daemon health check | GET /health | start |
A harness adapter that covers all rows has full parity with the reference runtime. Partial coverage is valid — the delta is explicit and auditable.
SDK Surface Additions #
The runtime expands SDK surface without breaking existing API.
TypeScript SDK (@signet/sdk)
New exports:
RuntimeAdapter
interfacecreateAdapter(harness, daemonUrl?)
— factory returning a pre-wired client implementing each lifecycle method as a daemon API callRuntimeAdapterServer
— HTTP server exposing adapter lifecycle as endpoints (for harnesses that prefer HTTP over library import)
Rust SDK
RuntimeAdapter
traitAdapterClient
struct — pre-wired daemon HTTP clientRuntimeAdapterServer
— axum-based server for HTTP attachment
Python SDK
RuntimeAdapter
abstract base classAdapterClient
— aiohttp-based daemon clientRuntimeAdapterServer
— FastAPI-based server for HTTP attachment
Any harness in any language can implement RuntimeAdapter
using the appropriate SDK, call the same daemon endpoints, and have full parity with the reference runtime.
Harness Adapter Pattern #
An adapter is a translation layer with no business logic. Example:
// @signetai/adapter-openclaw — full implementation
import { createAdapter } from '@signet/sdk'
export default function createPlugin(opts: { daemonUrl?: string }) {
const adapter = createAdapter('openclaw', opts.daemonUrl)
return {
onSessionStart: (ctx) => adapter.onSessionStart({
sessionKey: ctx.session.id, project: ctx.workspace
}),
onUserPromptSubmit: (ctx) => adapter.onUserPromptSubmit({
sessionKey: ctx.session.id, prompt: ctx.prompt
}),
onSessionEnd: (ctx) => adapter.onSessionEnd({
sessionKey: ctx.session.id
}),
onPreCompaction: (ctx) => adapter.onPreCompaction({
sessionKey: ctx.session.id, messageCount: ctx.messages.length
}),
onCompactionComplete: (ctx) => adapter.onCompactionComplete({
sessionKey: ctx.session.id, summary: ctx.summary
}),
}
}
When Signet adds a new capability, it adds a daemon endpoint and a new
RuntimeAdapter
method. Each harness adapter adds one translation. There is no duplicated business logic to reason about or diverge.
Build Sequence #
Phase 1: daemon-owned contract
- Preserve the daemon-owned execution contract: memory, hooks, secrets, and session state stay daemon-side
- Deliverable: supported harnesses share the same lifecycle semantics where their host APIs allow it
Phase 2: harness parity
- Keep Bun daemon, Rust daemon, and existing harness adapters aligned on the same daemon endpoints
- Treat external harnesses as thin adapters over the same runtime contract
- Deliverable: integration deltas are explicit and auditable
Phase 3: SDK adapter ergonomics
- Add or refine adapter helpers in the SDKs so external harnesses can implement the runtime contract with less boilerplate
- Keep adapter logic translation-only; business logic stays in the daemon/runtime boundary
- Deliverable: new harness integrations have a minimal documented path
Phase 4: optional runtime transport expansion
- If a supported harness exposes a stable local API, document it as an extension of the same runtime contract instead of a separate architecture
- Deliverable: transport choices can evolve without changing the core daemon contract
What This Is Not #
-
Not a replacement for the daemon. All state lives in the daemon. The runtime is stateless at the contract boundary.
-
Not a new memory system. Memory is still pipeline v2 + predictive scorer
-
knowledge graph, owned by the daemon.
-
Not breaking for existing harnesses. OpenClaw/Claude Code/OpenCode work through their own adapters and remain additive integrations.
-
Not a config system. Config lives in agent.yaml, read by the daemon.
Critical Files #
Runtime contract + adapters:
libs/sdk/
integrations/openclaw/connector/
integrations/claude-code/connector/
integrations/opencode/connector/
Daemon surfaces:
platform/daemon/
platform/daemon-rs/
Open Questions #
Provider config— provider selection and model live in agent.yaml. Keep the runtime-facing schema minimal and daemon-owned. - Multi-provider routing— route different task types to different providers when there is a concrete need, not as a prerequisite for the reference runtime. - Streaming in adapters— adapters should continue to translate lifecycle hooks cleanly without taking ownership of core runtime state. - Tool sandboxing— third-party tools run in-process by default. Consider stronger isolation only when the third-party tool surface grows. - Session resume— checkpoints stay daemon-owned; document resume behavior consistently across supported harnesses.