Understanding Agent Core's Design from Scratch
Agent Core solves an execution problem: once an application has spelled out its available capabilities, processing rules, and ways to finish, who guarantees that this processing runs in order and honors the budget, cancellation, and input/output contracts? Put it into your own scenario—you own a demo service, and colleagues @ you in the group chat every day asking "is the service online?". You want a helpdesk Agent: it answers automatically and can also explain clearly "why it answered this way": the answer comes from a real query receipt, not from merely claiming the service is online.
It does not decide for the application "what business it should run", nor does it require every Agent to grow into the same business flow. You can first answer with a single ordinary node, then add models, tools, materials, and checks to the same flow. When requirements grow, you just add the corresponding configuration.
You already know how to write the dumbest version: a script checks the status, then concatenates a sentence "the service is online". But it exposes itself as it runs, and you run into four troubles: once steps multiply, order and branching start to go wrong; data fetched once cannot be clearly tracked as to when it leaks into the next run; nobody backs the contracts for budget, cancellation, and getting results; and when you actually want to hook up models, materials, and tools, you do not know where to install them or how to avoid binding them to the core. This page starts from these four troubles and introduces the corresponding objects one by one. For a complete runnable version, see Build Your First Agent App; if you want to understand the execution process first, continue with Working Mechanism and Principles.
Meet three objects first
Start from the first three troubles. In the dumbest script, things get messy once steps multiply—that is "who defines the processing units and where they go"; data from one run cannot leak into the next—that is "who holds this run's data"; budget, cancellation, and result retrieval are not covered—that is "who guarantees the execution boundary". Each of the three troubles is caught by an object:
| Object | The question it answers | Example in the service Q&A |
|---|---|---|
| Workflow | What processing units exist, how are they connected, and when does it end? | Read status → compose answer → deliver |
| AgentCoreEngine | How does this run execute those definitions and control the running boundary? | Check rounds, run nodes, commit state, choose the successor |
| state | What does this run already know? | The user question, the actually fetched status, the answer to deliver |
Workflow is a reusable definition; state is the data of one execution. Do not treat the previous user's state as the next user's initial value, and do not store request identity or permissions in mutable fields of a shared definition.
Start with the version that has only one node:
import { AgentCoreEngine, defineWorkflow, route } from '@jangts/agent-core';
type State = { question: string; answer?: string };
const workflow = defineWorkflow<State, string>({
id: 'service-helpdesk',
nodes: [{
id: 'answer',
description: '回答演示服务状态',
run: () => ({ answer: '演示服务在线。' }),
routes: [route.deliver('answer')],
}],
});
const agent = new AgentCoreEngine({ workflow });
console.log(await agent.run({ question: '演示服务在线吗?' }));import { AgentCoreEngine, defineWorkflow, route } from '@jangts/agent-core';
const workflow = defineWorkflow({
id: 'service-helpdesk',
nodes: [{
id: 'answer',
description: '回答演示服务状态',
run: () => ({ answer: '演示服务在线。' }),
routes: [route.deliver('answer')],
}],
});
const agent = new AgentCoreEngine({ workflow });
console.log(await agent.run({ question: '演示服务在线吗?' }));This node returns { answer }, indicating the state fields it wants to update. route.deliver('answer') takes that field as the result of this agent.run. There is no model here, because this stage only teaches the execution contract; "using Agent Core" does not mean "every node must call a model". At this point the first three troubles are settled, and only the fourth remains: how do you install capabilities such as models, materials, and tools without binding them to the core?
Four responsibilities, three SDK roles
The fourth trouble arrives: models, materials, and tools are all capabilities held by the App, but the core must not conversely grow rigidly attached to a particular model or a particular knowledge base. So responsibilities are split into three layers, plus one real business owner; the diagram below shows their relationship.
The arrows in the diagram indicate assembly or invocation relationships. Core accepts implementations that satisfy the interface; it does not import Support or Kit.
Core, Support, and Kit are responsibility relationships within the SDK; the App is the owner of the actual business. They are not four runtime engine layers that must be traversed in sequence.
| Role | What it provides | What it does not decide for others |
|---|---|---|
| Core | The interfaces by which App calls Core and Core schedules App, plus execution guarantees | Does not own concrete business facts and permission rules |
| Support | Recommended implementations of Core contracts, policy completion, and configuration generation | Does not require Core to depend on it, and does not add a parallel scheduling protocol |
| Kit | Optional local business libraries for App, such as knowledge selection, evidence Q&A, artifacts, and persistent tasks | Does not turn its own fields into required fields for every Agent |
| App | Real dependencies, identity and authorization, capability lists, facts, business branches, and storage policies | Does not rely on model text to infer that a side effect has succeeded |
For example, to make the model answer based on real status: the App provides the status query service and authorization rules; Support can help organize tool schemas, registration, and model capability projection; Core executes the call, propagates cancellation, and validates the return; if knowledge must also be selected by section, the App additionally chooses to use the Knowledge Kit.
An Agent that does not use the Knowledge Kit does not need to invent documents, sections, or evidence fields just to integrate with Core.
Core accepts implementations of the contract
After splitting responsibilities, the next question comes immediately: in what shape do you hand your capabilities to Core? The answer is a unified contract, not some fixed way of writing code.
Whether a parameter is usable is decided by the Interface/type it satisfies. A plain object is one implementation form; a class instance also works as long as it satisfies the same contract. Support's products can be passed to Core directly without first being "converted into plain objects".
The two offline Providers below provide the same streaming interface and can both serve as provider in a model binding. They are only for observing the call process and have no real language understanding.
import type { ChatProvider, ModelMessage, ModelProviderContext } from '@jangts/agent-core/types';
const objectProvider: ChatProvider = {
async *stream(_messages, context) {
context.signal.throwIfAborted();
yield '演示服务在线。';
},
};
class OfflineProvider implements ChatProvider {
async *stream(_messages: readonly ModelMessage[], context: ModelProviderContext) {
context.signal.throwIfAborted();
yield '演示服务在线。';
}
}
const classProvider = new OfflineProvider();/** @type {import('@jangts/agent-core/types').ChatProvider} */
const objectProvider = {
async *stream(_messages, context) {
context.signal.throwIfAborted();
yield '演示服务在线。';
},
};
class OfflineProvider {
async *stream(_messages, context) {
context.signal.throwIfAborted();
yield '演示服务在线。';
}
}
const classProvider = new OfflineProvider();This does not mean any shape passes runtime checks. A Provider must have a callable stream, messages and chunks still follow the model protocol, and Core continues to validate budget and results. For parameters that genuinely require everything a Core constructor demands, follow the specific API's identity requirements; you cannot impersonate the object by copying a few fields.
Simplification comes from defaults, reuse, and references
Now that capabilities can be installed, you hit a new trouble: every added requirement means configuring the relationships once more. Real simplification is not about "saving a few parameters" but about fewer decisions and less repetition.
When learning the interfaces, the more valuable simplification is to decide fewer things and repeat the configuration of relationships less.
| Approach | What it does | How to add requirements later |
|---|---|---|
| Defaults | When the Workflow entry is omitted, the first node is used; a minimal Agent does not need a full model system configured first | When a non-first entry node is needed, add entryNodeId |
| Reuse | Workflow, Provider, and tool definitions are prepared once within the appropriate lifecycle | Modify existing definitions or immutable combinations and keep using the same run interface |
| Reference | Once a tool is bound, nodes call it by toolId; once multiple models are registered, select by ID | Add registered items, then specify IDs in the calls that actually differ |
For example, once a Provider is bound to the Agent, nodes use context.model. Nodes no longer pass fixed relationships such as the Provider, the model budget, or the current diagnostics parent node layer by layer. A request usually provides only the state that changes this time and the cancellation signal.
If the Workflow is known at construction time, pass it directly; if it must be assembled later, omit it first and then call bindWorkflow once. Both approaches establish a one-to-one binding between the Agent and the Workflow.
Core defaults differ from Support recommendations
Defaults and recommendations sound like the same thing, but they answer two different questions.
Core defaults answer "how to run when this parameter is missing". Support recommendations answer "in this specific scenario, what configuration is more appropriate".
For example, Core's model call limit is not a vendor capacity commitment, nor a recommended quota for every business. Support's scenario policies can combine the task, known model capabilities, and retention requirements to generate more suitable parameters or processing nodes; exact values and custom implementations must still be able to override these suggestions.
Support can provide different policies for scenarios such as quick replies, material Q&A, and restricted context, and allows exact parameter overrides. Every policy executes through the same Core interfaces.
Workflow keeps business relationships together
Back to the helpdesk: answering alone is not enough; it must first read the real status, then compose the answer. "Data must not cross over, steps must be ordered" here becomes two nodes plus one destination.
When the service Q&A needs to read a tool and then compose the answer, add nodes and routes to the original definition:
lookup: obtain the real status
↓
answer: compose the answer from the status
↓
deliver: return the checked answerA node owns one processing unit, a route states the destination, and the Engine executes. The entry only handles assembly and the request boundary; it does not need to know "when the fetched status is offline, which business branch to take".
Inside a node, ordinary functions organize the computation. A processing unit that needs independent scheduling, reuse, observation, or checks can be defined as a separate node.
The route function generates raw route configuration
Nodes exist—now how do you express the destination? "Take this path when offline, that path when online" is routing.
A raw route object is the complete public configuration. You can write it by hand, or use route/route.xxx to fix common judgments, express semantics, and generate the related identifiers.
import { route } from '@jangts/agent-core';
const handwritten = {
id: 'status-online',
conditionId: '{nid}.{rid}',
when: (state: { service: { status: string } }) => state.service.status === 'online',
targetNodeId: 'answer',
};
const generated = route.toIfEqual('answer', 'service.status', 'online');import { route } from '@jangts/agent-core';
const handwritten = {
id: 'status-online',
conditionId: '{nid}.{rid}',
when: state => state.service.status === 'online',
targetNodeId: 'answer',
};
const generated = route.toIfEqual('answer', 'service.status', 'online');Both versions use the same equality check and target, but the generated identifier is not necessarily identical to the handwritten one. Workflow applies the same validation and identifier handling to handwritten and generated routes; each generator provides its own default naming.
{nid} and {rid} in conditionId are placeholders available in raw configuration. Workflow evaluates them once it knows the node and the final route ID. toIfEqual and toIfEqualWith support dotted paths such as items.0.status; whether other methods accept a path depends on their own signature—do not assume "field" always means a nested path.
route.to* generally generates identifiers from the target or the judgment content, which is the convention of each specialized function. You can override them explicitly when you need stable external correlation identifiers. For duplicate ID handling and the specific templates, see the corresponding interface pages.
Extend the definition, keep the way it runs
The helpdesk will not always have only "check status". When you add review and explanation, do not overturn a flow that already works.
Workflow's add produces a new Workflow and leaves the old instance unchanged. It suits Agents that share a common base: share the lookup flow, then add review, explanation, or delivery nodes separately. A new Agent can use the combined definition directly.
fork creates a branch from an in-memory checkpoint produced by the current Agent. When the Workflow is omitted, it inherits the existing definition; when passed, it is combined internally with add. Application dependencies are provided explicitly through resume.
CoreStep supports procedural self-orchestration, where the application decides whether each step returns continue or complete. When a flow needs to show node and route relationships in one place, use Workflow.
Process observation and UI updates
After asking in the group chat, your colleague also wants to see "querying, composing". You do not want Core to touch the UI, so let it report facts only and let the App decide what to display.
By registering execution observers, the App can receive facts about models, tools, nodes, and so on before the task completes. The App decides which facts turn into UI states such as "querying" or "composing". Core does not operate the App UI, nor does it require the App to poll for progress.
A node can report its own usedModel flags, business payload, and sub-logs subs. Sub-logs are created through the execution context's createSubFlowLogger(): record the plan first with write, then complete the result with fix; execution identity, time, and correlation are provided by the Loop. When usedModel is not reported, it stays unknown, and sub-steps are not aggregated automatically. Side-channel observation and final content delivery have different guarantees: an observation failure does not change business execution; a content sink that must be awaited is consumed in order and preserves backpressure.
Finally, keep definitions, execution facts, and business authorization separate at all times. A definition saying "the service can be queried" does not prove that a query happened; a log saying "the model proposed a call" does not prove the tool succeeded; final wording saying "completed" cannot replace a real receipt. The following mechanism article explains these boundaries along a single run.
The mock model is also an optional Kit
To run offline before getting hands-on, swap the model for a reproducible fake model and leave everything else unchanged.
MockGPT is used to prepare repeatable replies and failure scenarios. The App can call generate directly, or inject fetch into an ordinary Provider; the Core executor does not depend on the simulator, and tool authorization, budget, and full parsing stay where they were.
import { MockGPT } from '@jangts/agent-core/mock';
const mock = new MockGPT({ rules: [{ prompt: '服务状态', generator: { online: true } }] });
console.log(await mock.generate({ model: 'demo', messages: [{ role: 'user', content: '服务状态' }] }));import { MockGPT } from '@jangts/agent-core/mock';
const mock = new MockGPT({ rules: [{ prompt: '服务状态', generator: { online: true } }] });
console.log(await mock.generate({ model: 'demo', messages: [{ role: 'user', content: '服务状态' }] }));Fixed results suit strict regression; input-dependent generators suit exploring interactions. Neither replaces validating the semantic quality of a real model.