集群扩展点

集群宿主平台在多个节点上运行长时运行的智能体会话。框架本身并不附带调度器或放置引擎,而是暴露一小组接缝:它定义决策点、发出结构化事件,并把策略交给宿主来提供。下文的所有内容都是你从框架外部接入的——你永远不需要分叉框架。

本页会明确区分哪些接缝在两个 SDK 中都可用(以 Node.js + Python 代码展示),哪些当前只在 Rust 核心中配置(没有 SDK 选项时以文字说明)。

身份标签

每个会话都可以携带四个不透明的身份标签。框架从不解释它们——它会把它们传播到钩子、追踪和 SessionData,并在恢复时还原它们。宿主正是借此把一个会话归属到租户、主体、智能体模板以及更广的关联链。

请将身份标签与 sessionStore / session_store 搭配使用,使标签在进程重启后依然保留。恢复时,由调用方提供的选项优先生效,因此你可以在节点之间迁移会话时为其重新打标签。

Node.js
Python
TypeScript
const session = agent.session('/path/to/project', {
tenantId: 'tenant-example',
principal: 'principal-example',
agentTemplateId: 'agent-template-example',
correlationId: 'trace-example',
});
// Getters return string | null
console.log(session.tenantId); // 'tenant-example'
console.log(session.principal); // 'principal-example'
console.log(session.agentTemplateId); // 'agent-template-example'
console.log(session.correlationId); // 'trace-example'

预算 / 成本守卫

预算守卫让宿主针对成本或令牌预算对每一次 LLM 调用进行把关。框架会在每次 LLM 请求之前调用你的守卫,并在请求返回之后再次调用。守卫是你自己拥有的策略;框架只负责执行你返回的决策。

Node.js
Python
TypeScript
session.setBudgetGuard({
checkBeforeLlm(ctx) {
if (overLimit(ctx.sessionId, ctx.estimatedTokens)) {
return {
decision: 'deny',
resource: 'tokens',
reason: 'monthly cap reached',
};
}
return { decision: 'allow' };
},
recordAfterLlm(ctx) {
meter(ctx.sessionId, ctx.usage);
},
});
// Clear the guard
session.setBudgetGuard(null);

Node 回调接收单个 ctx 对象,且绝不能抛出异常。请用 try/catch 包裹逻辑并返回显式决策。卡住或无法解析的 check* 回调会 fail-closed 为 deny。

两个 SDK 的决策结构完全一致:

返回值效果
None / null / { decision: 'allow' }继续执行 LLM 调用。
{ decision: 'soft', resource, consumed, limit, message? }发出 BudgetThresholdHit(kind 为 soft)并继续执行。
{ decision: 'deny', resource, reason }中止 LLM 调用。Python 抛出 RuntimeError("Budget exhausted...");Node 以 "Budget exhausted..." 拒绝(reject)。

这种健壮性是刻意为之,但两个 SDK 的失败语义不同:缺失的守卫方法会被当作宽松默认值处理。Python 回调出错会回退为 Allow。Node 回调不能 throw;超时或无法解析的 check* 返回会 fail-closed 为 deny。

集群事件词汇

宿主通过其钩子执行器,将集群级别的决策作为结构化的 AgentEvent 变体发出。会话内的钩子以统一方式订阅它们——与它们观察其他任何事件的方式相同——因此在宿主处编写的策略会原样呈现给智能体自身的钩子,无需特殊处理。

集群词汇如下:

  • BudgetThresholdHit { resource, kind, consumed, limit, message? } —— 预算守卫返回了 soft 决策(或宿主越过了它自己跟踪的某个阈值)。kind 用于区分软性警告与更硬性的限制。
  • PassivationRequested { reason, deadline_ms? } —— 宿主请求会话进入一个安全、可持久化的状态,以便将其从当前节点驱逐。deadline_ms 若存在,则表示强制驱逐前的宽限窗口。
  • PeerInvocation { from_session_id, from_tenant_id?, correlation_id? } —— 另一个会话调用了本会话。这些标签让接收方能够把调用归属回其源租户和关联链。

这些事件通过你的会话内钩子已经在使用的、经过验证的同一套钩子 API 来观察——Node 中为 session.registerHook,Python 中为 session.register_hook(参见钩子)。请将上述三个变体视为已记录在案的契约;宿主负责通过其钩子执行器发出它们。

确定性 ID 与时间(重放)

希望在另一节点上对某次运行进行逐位一致重放的集群,必须消除常规运行中两处不确定性的来源:随机 ID 和挂钟时间。Rust 核心将二者建模在一个 HostEnv { id_generator, clock } 之后。默认实现把 UUID 生成器与系统时钟配对;重放工具会换入 SequentialIdGenerator 和 FixedClock,使得对相同输入的重新执行在任意节点上都产生相同的 ID 和时间戳,从而产生相同的输出。

这是当前在 Rust 核心中配置的。它尚未暴露在 JS/Python 选项面上,因此没有对应的 Node/Python 代码——SDK 接入可能随后跟进。

循环检查点与运行恢复

配置了 sessionStore / session_store 后,智能体循环会在每一轮工具调用完成之后持久化一个检查点,以运行 id 作为键。任何共享同一存储的节点都可以重新水合该运行并继续它。

Node.js
Python
TypeScript
import { FileSessionStore } from '@a3s-lab/code';
const session = agent.session(workspace, {
sessionStore: new FileSessionStore('./.a3s/sessions'),
sessionId: 'session-from-node-a',
});
const result = await session.resumeRun('run-id-from-node-a');

系统会为恢复的工作分配一个新的运行 id——存储中的原始运行保持不变。有两条错误路径值得处理:

  • resume_run requires a session_store —— 未配置存储;回退到一个全新会话。
  • no loop checkpoint found for run 'X' —— 该运行从未到达其第一个检查点,或已被清理;稍后重试,或将该运行视为丢失。

由于检查点只在工具轮次之间、绝不在工具执行中途生成,恢复的运行永远不会重放一个执行到一半的工具。存储细节参见持久化。

长时运行会话的保留上限

运行数小时或数天的会话会在四个内存存储中累积状态:运行记录、每次运行的事件缓冲区、追踪事件,以及终态子智能体任务快照。若不加限制,它们会随会话寿命增长——对短寿命会话无妨,对长寿命会话则是真实的泄漏。

SessionRetentionLimits 为这四个存储分别设置上限。每个上限都是可选的:None 表示无上限的默认值。驱逐采用严格的 FIFO,并且正在运行的子智能体任务永不被丢弃——只有终态(已完成/已失败)快照会被驱逐。

Node 使用 retentionLimits,Python 使用 opts.retention_limits。Rust 宿主使用 SessionOptions::with_retention_limits(...)。字段名和示例见限制。


另见: 多机部署 · 持久化 · 限制 · 钩子