Skip to main content

Module runtime

Module runtime 

Source
Expand description

AgentBlockInProcessSpawnerFactory — in-process headless LLM agent execution over the agent-block-core SDK.

§Design responsibility — a state-less factory

The factory is a kind-level general-purpose builder — the process-wide infrastructure layer. It does not carry per-agent specialisation (script / system_prompt / tools); all agent specialisation belongs to AgentDef.spec + AgentDef.profile. The old default_script_path / default_project_root fields were removed — they were the collision source when a single process hosts multiple agent.md files.

§Two modes (via ScriptSource)

ModeTriggerChunk
PromptBasedAgent (default)spec.script_path absentScriptSource::Inline — the invoker build_inline_agent_invoker generates, which calls the SDK’s agent StdPkg module with the resolved mcp_servers embedded. NOT the SDK’s own DefaultAgent: that one passes no tools, so a frontmatter tools: line could never reach the model.
ScriptBasedAgentspec.script_path = "<path>"ScriptSource::Path(...) — a caller-provided Lua script, handed to the SDK verbatim.

Neither mode requires the script to agree on an event-kind string: the host_handler sink takes any kind as the terminal result (see “Per-task input and result” below for the one reserved exception).

profile.system_prompt (the agent.md body) is injected into the _CONTEXT Lua global through BlockConfig.context, and applies to both modes.

§Spec shape (AgentDef.spec)

This is the settled spec contract for AgentKind::AgentBlock (GH #86) — every key is optional, and every key the factory reads is listed here:

{
  "project_root": "<path>",          // optional, default = std::env::current_dir()
  "script_path": "<path>",           // optional; absent => PromptBasedAgent mode
  "mcp_rpc_timeout_ms": 30000,       // optional, default = 30s
  "mcp_servers": [                   // optional; the pool the tool grant selects from
    { "name": "outline", "command": "outline-mcp", "args": [] }
  ]
}

§Tool grant (GH #86)

The factory reads the effective tool set off profile.tools — which is already the resolved Runner::AgentBlockInProcess.tools whenever the agent declares that Runner, because compiler::project_bound_agent_for_legacy_factories overwrites profile.tools from the immutable BoundAgent snapshot (including with an empty list, so a Blueprint can revoke an agent.md’s inherited tools: line). No Runner declared → the agent.md line stands. There is deliberately no build hint for this axis: re-deriving the Runner here would bypass the pinned snapshot and let a Blueprint.runners edit change an in-flight Run’s grant on resume.

Enforcement is per mode:

ModeEnforcement
PromptBasedAgentEnforced at server granularity. Only the spec.mcp_servers entries named by an mcp__<server>__<tool> entry of the effective set are embedded into the invoker (resolve_needed_mcp_servers), so the LLM cannot reach an unlisted server — but it CAN reach every tool of a listed one (the SDK exposes a connected server’s full tool list). Grant per server, not per tool.
ScriptBasedAgentNot enforceable — the script drives its own mcp.connect. Declared mcp__ entries are therefore rejected at compile time rather than silently ignored; drop them and let the script own its connections.

Non-mcp__-prefixed names (Read / Write / WebSearch) do not select an MCP server and are inert in both modes (see [mcp_tools_of]) — the opts.extra_tools carry noted on resolve_needed_mcp_servers.

§Per-task input and result (GH #86)

Task context reaches this backend through one seam: WorkerInvocation::context, the in-process twin of WorkerPayload.context, filled once by InProcSpawner::spawn from the materialized AgentContextView. Nothing here peeks at Ctx directly, and no SpawnerAdapter wrapper re-resolves it — every Lua-visible surface below is derived from that one value:

Lua surfaceSource
_PROMPTThe step’s evaluated in, via inv.promptBlockConfig.prompt. A String — a structured in arrives JSON-stringified, so a script that wants a table calls std.json.decode(_PROMPT).
_CONTEXTprofile.system_prompt, via BlockConfig.context.
TASK_METADATA_GLOBAL (_TASK_METADATA)view.task_metadata (the launch’s init_ctx.task_metadata bag), set through the SDK’s extra_globals — converted natively, so nesting survives and the chunk text is never rewritten.
AGENT_CTX_GLOBAL (_AGENT_CTX)view.extra — the Blueprint-declared agent context (default_agent_ctx / AgentMeta.ctx, GH #21) after ContextPolicy filtering.

Both come from context_globals, the single place this mapping is decided; the Lua in-process worker renders the same two globals from it, so a gate is portable between the two backends. view.steps (prior-step OUTPUT pointers) is deliberately NOT rendered — see the carrier note in crate::core::agent_context.

No server-process env is involved in any of them. The per-task working directory is not a Lua global — it becomes the SDK’s project_root (see the next section), which surfaces to a script as std.env.project_root() and as the default cwd of sh.exec and of MCP servers spawned by mcp.connect. It does NOT chdir the host process, so a bare io.open("rel/path") still resolves against the server’s own cwd.

A script returns its result by calling bus.emit(<kind>, payload)not by returning a value from the chunk. Two destinations:

emit kindEffect
ARTIFACT_EVENT_KIND (artifact)Stages a named part through WorkerInvocation::sink ({name = ..., content = ...}) and leaves the invocation running. Any number of these.
anything elseThe terminal result, first emit wins. [WorkerResultCaptor] normalises the payload into WorkerResult.value (payload.contentpayload.response → the whole payload).

Both verdict channels are therefore reachable: VerdictChannel::Body compares the terminal value, and VerdictChannel::Part compares a staged "verdict" part — stage it with bus.emit("artifact", {name = "verdict", content = "PASS"}) before the terminal emit, and the plain body stays free for the report.

§project_root resolution (issue #17, GH #20)

spec.project_root (above) is only the compile-time fallback tier — resolved once in [AgentBlockInProcessSpawnerFactory::build], before any Ctx exists. Per invocation, [resolve_project_root] applies the task-context tier off WorkerInvocation::context (GH #20 Contract C — see crate::core::agent_context for the full narrative) with this priority (highest first):

  1. view.work_dir — Task-level, set by TaskInputMiddleware from the launch’s init_ctx.work_dir.
  2. view.project_root — same middleware, init_ctx.project_root.
  3. spec.project_root / std::env::current_dir() (the compile-time fallback baked into [AgentBlockSettings] above).

This lets a single Blueprint’s AgentDef.spec.project_root (fixed at compile time) be overridden per task launch, so the same Blueprint can run against different caller-supplied project roots without a spec edit.

§SDK paths introduced from v0.22.0 through v0.27.0

VersionFeatureUse case
v0.22.0bus.emit(kind, payload, id?) Lua bridgescript → host event push
v0.23.0BlockConfig.host_handlersPre-install a Rust handler on the EventBus
v0.24.0BlockConfig.auto_serve_busSDK embed drives the dispatcher in the background
v0.25.0BlockConfig.shutdown_token + BlockError::Cancelled + Send on run()tokio::spawn and external cancel
v0.26.0ScriptSource / PromptSource / SecretKeySource enums plus the embedded DefaultAgent invoker (breaking)Script becomes optional at the SDK level
v0.27.0Embed the compile_loop StdPkg into corerequire("compile_loop") hits directly

Structs§

AgentBlockInProcessSpawnerFactory
The SpawnerFactory for AgentBlock. KIND = AgentKind::AgentBlock.
AgentBlockWorker
Concrete Worker type for the AgentBlock kind — the handle for an LLM call routed through the agent-block-core SDK. Embeds a WorkerJoinHandler to carry the async signal. The intent is to eventually keep the SDK-specific quirks — the agent_result event name, payload shape, shutdown-token bridging, agent_result.content normalisation — contained inside this struct. Today it lands as a thin shape holding only the async signal; Phase B adds the normalisation layer here and structurally eliminates the token-boilerplate waste observed in observation #2.

Constants§

AGENT_CTX_GLOBAL
The Lua global carrying the Blueprint-declared agent context — the AgentContextView.extra bag, which AgentContextMiddleware fills from Blueprint.default_agent_ctx / AgentMeta.ctx (GH #21) after applying ContextPolicy.
ARTIFACT_EVENT_KIND
The one bus.emit kind this backend reserves: an emit under it stages a named part instead of completing the invocation (GH #86).
TASK_METADATA_GLOBAL
The Lua global carrying the launch’s init_ctx.task_metadata bag into a script, set through the SDK’s extra_globals (agent-block-core v0.30+).

Functions§

build_inline_agent_invoker
Build the inline Lua script used on the PromptBasedAgent path (when spec.script_path is absent). Instead of the SDK’s embedded DEFAULT_AGENT_INVOKER (which passes no tools), this embeds mcp_servers as a Lua literal table and hands it to agent.run.
context_globals
The Lua globals this backend derives from the materialized context view — the single place the mapping “view field → Lua global” is decided, so crate::blueprint::compiler’s Lua worker can render the same surface and keep a gate portable between the two in-process backends.
resolve_needed_mcp_servers
Cross-reference the agent’s declared tool set (see [resolve_effective_tools] for which tier that comes from) with spec.mcp_servers (the "server name" → command + args mapping provided by the AgentDef literal cascade) and resolve the mcp_servers config actually exposed to the LLM for this invocation.