Named Services: The Interface Between Agents And App Realms
An app owns a realm — task:, mem:, cnv: — with its own schema, search,
actions, and rendering. Named services let a ReAct agent enter that realm without learning any
of its private domain rules. The agent gets one generic interface; the provider remains the
owner of meaning. This Deep piece walks the four agent surfaces, the pull/read materialization path, and
every rendering policy the provider controls.
Named services are the interface that lets an agent enter a new app realm without that agent or its host learning the app's private domain rules.
An app owns a namespace such as task:, mem:, cnv:, repo:,
or a future domain. It registers a named-service provider. A consuming app configures which provider operations
are allowed for its agent, UI, scene, pinboard, jobs, or external callers. ReAct then gets a small set of
generic tools and policies that can explore, materialize, render, and mutate objects in that realm.
The important boundary is this: the generic agent runtime owns react.pull /
react.read, the named_services.* tool calls, timeline storage, strategy governance,
and the configured access policy. The provider app owns the object_ref grammar,
object schema, search spaces, object capabilities/actions, mutation rules, bytes/materialization, block
production, and render and compaction projection.
The agent sees one universal interface. The provider remains the owner of meaning.
Why this exists
Without named services, every agent-facing integration becomes bespoke:
mem: or task: prefixes;That does not scale. It also breaks provenance: once an object is copied into a chat message, pinboard card, or generated file, the system can lose which realm owned the evidence.
Named services solve this by making the owner realm explicit and reusable. For an
object_ref = task:issue:ticket_123, the task provider owns the schema, search,
preview, open action, mutation, and model-visible rendering — while chat, scene, pinboard, and
ReAct own where the object is shown, which operations are allowed, and how the returned provider
result is routed.
object_ref = task:issue:ticket_123
task provider owns:
schema
search
preview
open action
mutation
model-visible rendering
chat, scene, pinboard, ReAct own:
where the object is shown
which operations are allowed
how the returned provider result is routed
The object keeps the same object_ref as it moves between chat, scene, pinboard, ReAct, MCP/API
calls, Data Bus jobs, and cron outputs.
The four agent interfaces
Named services touch an agent in four different places. They should not be collapsed into one "tool" concept.
1. Model-callable tools. named_services.provider_about,
named_services.search_objects, named_services.object_schema,
named_services.upsert_object, and so on.
2. Materialization path. react.pull(object_ref) calls the provider
object.get(response_mode=stream), which writes a fi: workspace artifact;
react.read(fi:) then dispatches to the owner block.produce.
3. Prompt rendering policies. timeline_projection,
announce_production, compaction_projection, and the provider
block.render hook.
4. UI action path. object.resolve and
object.action(open/download/preview) resolve a ui_event.target_surface, which the
scene, pinboard, or chat route to a mounted component.
The model-callable named-service tools are only one surface. The richer interface is the combination of tools, pull/read, rendering policies, and UI actions.
Note that block.produce, block.render, event.resolve, and
object.get(response_mode=stream) are provider operations the ReAct infrastructure calls
during materialization and rendering — they are not model-callable
named_services.* tools.
Provider registration
A provider app registers the namespace it owns and the operations it supports. The registration is not just a Python class. It is the public contract for how the realm can be searched, read, displayed, and changed.
@named_service_provider(
provider_id="task.issue",
namespace="task",
operations={
"provider.about": {"transports": ["bundle_registry"]},
"object.search": {"transports": ["bundle_registry"]},
"object.schema": {"transports": ["bundle_registry"]},
"object.get": {"transports": ["bundle_registry"]},
"object.resolve": {"transports": ["bundle_registry"]},
"object.action": {"transports": ["bundle_registry"]},
"object.upsert": {"transports": ["bundle_registry"]},
"object.delete": {"transports": ["bundle_registry"]},
"block.produce": {"transports": ["bundle_registry"]},
"block.render": {"transports": ["bundle_registry"]},
},
search_scopes=[
{
"namespace": "task:issue",
"label": "task issues",
"object_kind": "task:issue",
},
{
"namespace": "task:attachment",
"label": "task attachments",
"object_kind": "task:attachment",
},
],
)
class TaskIssueProvider(NamedServiceProvider):
...
At app startup, the app exposes a registry:
def named_services(self):
registry = NamedServiceRegistry()
registry.register(TaskIssueProvider(...))
return registry
The runtime records the provider in Named Service Discovery. Consumer apps do not need to hardcode the provider location. They configure access to the namespace and let discovery pick the registered provider for the current tenant/project context.
named_services() registry and lands a record in the discovery table; a consumer call on the namespace is matched to that provider and a NamedServiceClient invokes the owned operations.Consumer registration for agents
The consumer app decides which namespace operations its agent may use. This is configured under
surfaces.as_consumer, because the agent is one consumer surface among others.
surfaces:
as_consumer:
default_agent: main
agents:
main:
tools:
- id: task_service
kind: named_service
alias: named_services
namespaces:
task:
allowed:
- provider.about
- object.search
- object.schema
- object.get
- object.host_file
- object.upsert
- object.delete
tool_traits:
provider_about:
strategy: [exploration]
search_objects:
strategy: [exploration]
object_schema:
strategy: [exploration]
get_object:
strategy: [exploration]
host_file:
strategy: [exploitation]
upsert_object:
strategy: [exploitation]
delete_object:
strategy: [exploitation]
event_sources:
- kind: named_service
namespace: task
enabled: true
discovery:
mode: service_discovery
policies:
pull:
mode: provider
operation: object.get
block_production:
mode: provider
operation: block.produce
ui:
canvas:
resolvers:
- kind: named_service
namespace: task
enabled: true
discovery:
mode: service_discovery
allowed: [object.resolve, object.action]
There are three separate permissions here. agents.<agent>.tools exposes the
model-callable named_services.* operations. agents.<agent>.event_sources enables
the ReAct pull/read/render path for owner objects. ui.canvas.resolvers enables the scene / chat /
pinboard object.resolve and object.action.
agents.<agent>.tools — model-callable named_services.* operationsagents.<agent>.event_sources — ReAct pull/read/render path for owner objectsui.canvas.resolvers — scene/chat/pinboard object.resolve and object.actionA namespace can be pullable by react.pull without exposing
named_services.get_object to the model. A namespace can support UI open actions without exposing
object_action as a model-callable tool.
Canonical named-service tools
The current generic ReAct tool adapter exposes these model-callable tools when the consumer config allows the corresponding provider operations.
| Tool | Provider operation | Usual strategy | Purpose |
|---|---|---|---|
named_services.provider_about |
provider.about |
exploration | Understand what the provider owns and which objects/scopes it exposes. |
named_services.list_objects |
object.list |
exploration | Page through a bounded collection when listing is meaningful. |
named_services.search_objects |
object.search |
exploration | Search a base namespace or provider-declared search scope. Emits generic search-result context rows. |
named_services.get_object |
object.get |
exploration | Read one bounded object envelope. For exact/large bytes, prefer react.pull then react.read. |
named_services.object_schema |
object.schema |
exploration | Ask the provider for fields, object kinds, filters, upsert/delete payloads, and usage guidance. |
named_services.host_file |
object.host_file |
exploitation by default | Move a ReAct artifact/file into provider-owned storage and receive a provider-owned ref. |
named_services.upsert_object |
object.upsert |
exploitation by default | Create or update a provider object according to provider schema. |
named_services.delete_object |
object.delete |
exploitation | Delete, archive, retire, or suggest deletion according to provider policy. |
named_services.object_action |
object.action |
action-dependent | Run bounded provider actions such as preview/open/download/describe. Usually used by UI resolvers, not exposed broadly to the model. |
The strategy column is not hardcoded truth. It is deployment policy. For example, memory recording may be
configured as neutral if the product treats it as side-memory rather than an answer-changing write.
A provider action that only describes an object may be exploration, while a provider action that sends mail or
changes status is exploitation.
The agent sees traits in the tool catalog:
Scope:
- namespaces applicable: task, mem, cnv
- strategy: exploitation (default)
- strategy overrides by namespace:
- mem: neutral
That trait is used by online ReAct governance, which classifies every tool against four trait values —
exploration, exploitation, neutral, and unknown. In the same
model round, exploration -> exploitation is denied because the exploitation would depend on a
result that has not yet been shown to the model. exploitation -> exploration is allowed for
staged work.
Searchable namespaces and search scopes
A namespace is not necessarily one flat object space. A provider can declare search scopes:
task
task:issue
task:attachment
sensor
sensor:temperature
sensor:humidity:aggr
cnv
cnv
Search scopes are provider registration metadata. They let the tool catalog show the model where search is
meaningful without making a live provider.about call first.
search_scopes:
- namespace: task:issue
label: task issues
object_kind: task:issue
filters:
status:
type: string
enum: [open, in_progress, blocked, resolved]
The consumer authorizes the base namespace:
namespaces:
task:
allowed: [object.search]
The model can then call a scoped namespace:
{
"namespace": "task:issue",
"query": "membership cancellation",
"filters": "{\"status\":\"open\"}"
}
The provider owns what the scope means. Search may be lexical, semantic, hybrid, RRF-ranked, memory-weighted, or something domain-specific. The provider also owns filter names and relevance tuning.
provider.about call.Object schema before mutation
The agent should not infer object bodies from visual cards or old examples. It asks the provider:
named_services.object_schema(namespace="task", object_kind="task:issue")
The schema can describe:
object_ref shape;Then the model calls mutation with provider-schema JSON:
{
"namespace": "task",
"object_ref": "task:issue:ticket_123",
"base_revision": "7",
"object_json": "{\"status\":\"blocked\",\"comment\":\"Waiting for contract confirmation.\"}"
}
Payload fields are provider-owned. They do not control named-service routing. Routing is controlled by the tool name, configured namespace, operation, and provider discovery.
Pull, read, and owner block production
named_services.get_object is not the main exact-content path. The ReAct object materialization
path is:
react.pull(paths=["mem:record:mem_123"])
-> namespace rehoster
-> provider object.get(response_mode=stream)
-> local artifact:
logical_path = fi:turn_1.files/mem_123.json
physical_path = turn_1/files/mem_123.json
object_ref = mem:record:mem_123
scope = files or snapshots
react.read(paths=["fi:turn_1.files/mem_123.json"])
-> generic read target with object_ref
-> owner event source resolution
-> provider block.produce
-> model-visible owner blocks
scope is workspace materialization metadata. It is not the owner namespace. The owner identity
remains object_ref.
react.pull materializes bytes with object_ref preserved; react.read hands them to the owner's block.produce.For a volatile object, the owner may intentionally produce only a compact timeline fact and put the rich current view in ANNOUNCE. Canvas is the reference example:
react.pull(paths=["cnv:main"])
-> fi:turn_1.snapshots/cnv/main.json
react.read(paths=["fi:turn_1.snapshots/cnv/main.json"])
-> [CANVAS TOOL RESULT] timeline fact
-> [CANVAS BOARD] in ANNOUNCE for N render rounds
The compact fact tells the model how to refresh:
announce_effect: board projection refreshed in ANNOUNCE for 3 render rounds
refresh_rule: use react.pull(paths=['cnv:main']) and react.read on the returned fi: path if you need an updated or prolonged board view
This text belongs to the canvas owner policy. Generic ReAct does not know that cnv: means canvas
or that the object is volatile.
For stats_only reads, the owner can emit top-level original_object_stats. ReAct
copies that into the stats response without interpreting domain fields.
Rendering policies: what the model sees
Named-service integration is not complete unless the provider controls model-visible representation. There are several policy surfaces.
block.produce
called during react.read
turns one object/read target into bounded timeline blocks
timeline_projection
render-time mutation of already-produced timeline blocks
good for compact facts instead of raw JSON
announce_production
non-durable prompt-tail context
good for current focused state, board maps, active forms
block.render
provider hook called during prompt rendering
can patch provider-owned visible blocks
compaction_projection
representation for summarizer/compaction input
should preserve stable refs and facts, not prompt-only ANNOUNCE text
compaction_projection shapes what survives compaction.The compaction phase seam exists in the code today: the compaction_projection hook is already
wired so providers can shape summarizer input, though some preservation is still hardcoded rather than
provider-owned. The direction is that providers fully own compaction rendering. A task provider can decide which
issue fields survive summarization. A memory provider can decide which salience/freshness fields matter. A
canvas provider can preserve cnv:main@52 and selected card ids while not treating the rendered
ANNOUNCE board map as authoritative state.
Compaction projection answers a different question than normal prompt rendering:
normal render:
What should the model see right now to act?
compaction render:
What stable facts and refs should survive when this block leaves the visible
window?
Provider policies should therefore keep recovery anchors explicit:
object_ref: task:issue:ticket_123
revision: 7
status: blocked
recover_with: react.pull(paths=["task:issue:ticket_123"])
UI actions and pinboard
The same provider contract is used outside the model loop. A canvas card stores
object_ref = task:issue:ticket_123. When the user clicks open, canvas/scene calls
object.action(open, object_ref). The task provider returns a
ui_event.target_surface = task_tracker.issue_editor with an action and payload, and the scene routes
that target_surface to the mounted component alias.
canvas card stores:
object_ref = task:issue:ticket_123
user clicks open:
canvas/scene calls object.action(open, object_ref)
task provider returns:
ui_event.target_surface = task_tracker.issue_editor
ui_event.action = open
ui_event.payload = { issue_id: ... }
scene routes:
target_surface -> mounted component alias
object_ref; the provider decides the action.Canvas and scene do not decide that task: means issue editor. They pass the full
object_ref to the provider resolver. Namespace presentation config provides colors, icons, and
labels. Provider resolver results provide capabilities and actions.
Pinboard stores proxy cards. The card layout belongs to canvas. The referenced object still belongs to its provider realm.
Pinboard card
layout: x/y/w/h, comments, description, selected state
object_ref: mem:record:mem_123
|
v
memory provider
preview/open/read/search/mutate memory record
How to add a new agent realm
For a new app realm, implement this checklist.
Provider app:
- Choose canonical
object_refgrammar. - Define object kinds and namespace presentation config.
- Implement
NamedServiceProvider. - Expose
named_services()registry. - Register provider operations and search scopes.
- Implement
provider.aboutandobject.schema. - Implement search/list/get as needed.
- Implement
object.get(response_mode=stream)if ReAct should pull objects. - Implement
block.produceforreact.read. - Implement
block.renderif provider-owned blocks need prompt-time patches. - Implement
compaction_projectionpolicy or provider compaction renderer when the realm needs custom summary/recovery shape. - Implement
object.resolveandobject.actionfor UI surfaces. - Implement mutation operations only with provider-side schema validation.
Consumer app:
- Add namespace under
surfaces.as_consumer. - Allow only the operations each agent/surface needs.
- Configure
tool_traitsfor ReAct governance. - Configure named-service event source/pull/block-production policy.
- Configure UI resolvers for canvas/chat/pinboard/scene.
- Add scene surface command contracts for provider-returned
target_surfacevalues. - Test discovery logs, tool catalog, search scopes, object actions, pull/read, and compaction behavior.
Useful diagnostics
When the agent cannot see a provider tool:
check surfaces.as_consumer.agents.<agent>.tools
check namespace allowed operations
check named_services tool catalog
check strategy traits rendered in tool scope
When a provider cannot be found:
Named-service discovery provider scan
Named-service endpoint resolution
selected_provider: <none>
reason: no matching provider
When search scopes are missing:
[named_services.tools.search_scopes]
registry_keys=...
discovery_entries=...
entry_scopes=...
result_counts=...
When a pulled object reads as a generic file:
react.read.owner_projection
status=no_event_source | no_blocks | policy_error | produced
When a UI card stays "resolving":
object.resolve / object.action logs
provider ui_event present?
namespace presentation config present?
surfaceCommandContracts target_surface mapped?
When a canvas board appears twice in ANNOUNCE:
check whether chat.canvas.state and canvas.read both refer to same cnv:<board>
owner announce policy should consolidate by object identity
Documentation on GitHub
The live docs behind this entry:
- Named services — overview
- Provider registration & operations
- Consumer clients & discovery
- Consumer integration config
- object_ref presentation & actions
- ReAct object materialization (pull/read)
- ReAct object policy bridge
- Resolver & policy registration
- Online strategic governance
- Tool strategy traits
- Block production
- Announce production
- Compaction projection