013: Agent/Scopes
- Requires:
A mechanism for making a controlled subset of context from a parent environment available to an execution. The _scopes
property acts as an allow-list, defining a focused and secure view of the data a Call can access.
The Scoped context pattern is a fundamental mechanism for managing the context available to a Call. In a complex agentic system, a Call rarely executes in a vacuum; it often needs access to information from its parent environment, such as user input, current state, or the results of previous steps. Scoped context provides a secure and explicit way to control this flow of information.
By restricting the context, scopes enhance security, prevent accidental data leakage, and focus the LLM, leading to more predictable and cost-effective executions. This controlled context is also the key to modularity, allowing components like Ideas and Activities to be truly self-contained and reusable. This document explains how this pattern works and how it composes with other agent capabilities.
Provisioning vs. Requesting Context
The schema for the _scopes
property determines whether context is statically provisioned or dynamically requested at runtime.
-
Static Scopes (Context Provisioning): The
_scopes
schema can be aconst
value, which means the context is provisioned. The designer has hard-coded the exact context the tool is allowed to see.{ "_scopes": { "const": ["input"] } }
-
Dynamic Scopes (Context Requesting): The
_scopes
schema can be more flexible, allowing the context to be requested. The LLM decides which of the available scopes it needs to generate the Call.{ "_scopes": { "type": "array", "items": { "enum": ["state", "input"] } } }
This dynamic pattern is especially powerful when combined with a human-in-the-loop approval system, providing a critical layer of transparency and control.
The Role of Scopes in Call Composition
The _scopes
property is the primary mechanism for controlling the context available to a Call. It acts as an allow-list, filtering the parent environment to provide a focused, limited field of view for the execution. This controlled context is fundamental to how a Call is processed, and its role adapts to support a compositional model of execution where different capabilities like explicit logic, instancing, and modularity can be combined.
-
Latent Execution: In the default latent execution,
_scopes
serve as a "nudge" to focus the LLM's attention on relevant parts of the parent context. This is a best-effort guide, not a strict filter, but it is crucial for improving the reliability and cost-effectiveness of LLM-driven reasoning by reducing noise from irrelevant data. See the Disambiguation with Scopes example. -
Explicit Execution (
_activity
): When a Call is backed by a deterministic Activity, the role of scopes becomes more direct. The scoped context is passed wholesale to the Activity function as an additional parameter. This gives the Activity full access to the necessary contextual data, even if that data wasn't directly used by the LLM to generate the Call's primary parameters. See the Providing Context to an Activity example. -
Instancing (
_instance
): In a multi-instance request where the agent processes a batch of similar data objects, scopes become instance-aware. The protocol ensures that a Call targeting a specific instance receives only the context for that instance. This is critical for maintaining data integrity and preventing context from "leaking" between parallel executions. -
Delegated Isolation (
_delegate
): When a Call is delegated to an external Delegate, scopes act as the gatekeepers of context. They define what from the parent's environment gets appended to the delegate's own internal context, creating the final context for the isolated sub-request. Nothing from the parent is available unless explicitly scoped, ensuring true encapsulation. See the Scoping a Delegate's Context and Instancing with Scoped Delegates examples.
The _scopes
property is the bridge that allows a Call to receive context. This chapter has shown how this single mechanism adapts to different execution modes—from gently guiding a latent call to strictly defining the entire context for a delegated one. This flexibility is what makes it a cornerstone of building complex, secure, and modular agentic systems.
Examples
Example: Disambiguation with Scopes
Ambiguous Context
In this example, the context is ambiguous. It contains two different users: the currentUser
in the state
and a mentionedUser
in the input
. The agent needs to send a message, but it's unclear to whom.
[
{
"type": "state",
"currentUser": { "id": "user_A", "name": "Alice" }
},
{
"type": "input",
"mentionedUser": { "id": "user_B", "name": "Bob" },
"instruction": "Send a welcome message to the user mentioned above."
}
]
`Call` with Scope
By adding _scopes: ["input"]
, the caller provides a crucial hint. It tells the LLM to focus on the input
message, effectively resolving the ambiguity and ensuring the message is sent to the correct recipient, Bob.
{
"_tool": "sendMessage",
"_scopes": ["input"],
"recipientId": "user_B",
"message": "Welcome, Bob!"
}
Example: Providing Context to an Activity
Here, an Activity needs access to contextual information that isn't a direct parameter of the tool. The logEvent
tool only takes an eventName
, but the underlying activity also needs to know the userId
to function correctly.
`Call` with Scope
The Call is simple, only providing the eventName
. However, the _scopes: ["state"]
property tells the execution engine to pass the state
object to the activity.
{
"_tool": "logEvent",
"_scopes": ["state"],
"eventName": "user_login"
}
Activity Implementation (TypeScript)
The Activity is registered with a function that destructures its parameters directly from the call and the scoped context. This provides clean, direct access to both the eventName
and the userId
.
// The parameters are destructured to directly access 'eventName'
// from the call and 'state' from the scoped context.
Activity.register('logEvent', async ({ eventName }, { state }) => {
const userId = state.userId;
await analytics.track(eventName, { userId });
});
Example: Scoping a Delegate's Context
When delegating, _scopes
define what from the parent context gets appended to the delegate's own internal context. Here, a high-level Orchestrator
delegates a task, passing the entire state
object to a specialized Summarizer
delegate.
Parent `Call` with Scope
The Orchestrator
agent has its own context. It makes a Call
that scopes the state
to be included with the delegate's context.
// Orchestrator's Context and Call
[
{
"type": "state",
"articleText": "A long and complex article..."
},
{
"_tool": "summarizeArticle",
"_delegate": "SummarizerAgent",
"_scopes": ["state"]
}
]
Delegate's Sub-Request Context
The Summarizer
's execution environment is composed of its own internal context plus the data scoped from the parent. This allows for modularity while still providing necessary information.
// Context constructed for the Summarizer's sub-request
[
// The Summarizer's internal context
{
"type": "system",
"message": "You are an expert summarizer."
},
// The scoped context from the parent is appended
{
"type": "state",
"articleText": "A long and complex article..."
}
]
Example: Instancing with Scoped Delegates
This example shows how _scopes
and _instance
compose to allow a single agent to orchestrate multiple, isolated delegate calls in parallel.
Parent Context & Solution
The parent agent has two distinct State
instances. It generates a solution
with two Calls
to a translatorDelegate
. Each call targets a different instance and correctly scopes only the state
for that instance.
// PARENT CONTEXT
[
{ "type": "state", "_instance": "①", "text": "Hello" },
{ "type": "state", "_instance": "②", "text": "Bonjour" }
]
// LLM'S SOLUTION
{
"calls": [
{
"_tool": "translate",
"_delegate": "translatorDelegate",
"_instance": "①",
"_scopes": ["state"]
},
{
"_tool": "translate",
"_delegate": "translatorDelegate",
"_instance": "②",
"_scopes": ["state"]
}
]
}
Delegate Sub-Request Contexts
The execution engine creates two independent sub-requests. The _instance
and _scopes
properties work together to ensure that each delegate receives only its own, correctly-scoped state
.
// CONTEXT FOR DELEGATE ①
[
{ "type": "system", "message": "You are a translator." },
{ "type": "state", "text": "Hello" }
]
// CONTEXT FOR DELEGATE ②
[
{ "type": "system", "message": "You are a translator." },
{ "type": "state", "text": "Bonjour" }
]