会话

Agent 持有配置和服务提供商状态。Session 把这个智能体绑定到一个工作区和一次 对话生命周期。

界面通常从这里开始接入:订阅 Session 产生的 AgentEvent,再把事件映射为进度、 工具调用、权限确认和结果。

session.tsTypeScript
import { Agent } from '@a3s-lab/code';
const agent = await Agent.create('agent.acl');
const session = agent.session('/repo', {
model: 'provider/model-id',
planningMode: 'enabled',
goalTracking: true,
autoDelegation: { enabled: true, maxTasks: 4 },
autoParallel: false,
});

Rust 构建路径

Rust 会话构建以异步为先,因为默认内存存储、文件型存储、队列、轨迹记录与 MCP 发现都可能需要 I/O:

Rust
let session = agent
.session_builder("/repo")
.options(options)
.build()
.await?;

session_async、resume_session_async、session_for_agent_async 与 session_for_worker_async 是直接的异步入口。Node 和 Python 保留现有工厂 命名,由原生绑定在内部委托给同一个异步构建内核。

同步 Rust Agent::session 是严格兼容路径:只接受已经显式初始化好的资源,绝不 启动或阻塞异步运行时。仍需初始化的默认或文件型内存存储、文件会话存储、队列、 轨迹记录器,以及 SessionOptions 中的任何宿主 MCP 管理器都会返回 CodeError::AsyncSessionBuildRequired;会话选项中的 MCP 能力发现始终是异步的。 应改用构建器,不要捕获错误后悄悄更换后端。 同步路径只能继承智能体初始化时已经缓存的全局 MCP 工具。

planningMode 是显式三态:'auto' 使用默认结构化预分析,'enabled' 强制规划,'disabled' 在低延迟调用中关闭规划。Node.js 和 Python 还接受布尔 planning 选项,Rust 提供 with_planning(bool):不设置等同 'auto',true 等同 'enabled',false 等同 'disabled'。两者同时设置时以 planningMode 为准。Go 只提供 PlanningMode。

规划会写入运行级状态。宿主应用可以把这些状态渲染成任务列表,并随着运行事件更新 每一项,而不是从文本令牌中猜测进度。

单任务操作约束

同一个会话同时只准入一个会影响对话记录的操作。send、stream、它们的附件变体、 斜杠命令与 resumeRun 共用快速失败准入门。重叠调用会在读取历史或派发命令前返回 CodeError::SessionBusy,不会排队 等待当前操作。

开始下一次对话操作前,应等待活动结果、把事件流消费到结束,或先取消它。 即使公开的事件流句柄被丢弃或中止,运行时也会在生产者真正停止前继续 持有租约。直接调用宿主工具的辅助方法不改变对话记录,因此不占用这把租约。

Node 与 Python 的事件流迭代器会在终止边界等待这段生命周期清理。迭代器完整消费 并报告结束后,立即开始下一次对话操作不会继承上一个事件流留下的过期忙碌状态。

Run 如何推进

每个会话有一份事实日志,位于 <workspace>/.a3s/effect-log/<thread>.jsonl。 send 或 stream 把提示词作为 user.message 事实追加,然后折叠日志来选择每一步: 调用模型、执行工具、压缩、停靠或结束。一次运行最多 32 步,超过后以 StepLimit 错误停止。完整模型见事实日志控制。

这对宿主意味着:

  • 确认会停靠。 需要批准的工具调用会发出 confirmation_required 并等待。用 confirmToolUse 回答(Rust 与 Python 为 confirm_tool_use,Go 为 ConfirmToolUse)。答案记录为事实,因此重启后同样有效。日志本身没有超时。
  • 提问会停靠。 ask_user 调用会发出 user_question 并等待回答。
  • 每轮工具工作有上限。 内置工具预算在 8 次成功工具结果后结束本轮。工具轮次 上限(maxToolRounds,Python max_tool_rounds,Go MaxToolRounds)会让下一次 模型调用看不到工具,只能用文本回答。
  • Steer 是事实。 已应用的 steer 是另一条 user.message,因此也会重置这两个 计数器。

发送消息

Rust
Node.js
Python
Go
Rust
let result = session
.send("审查这个仓库并列出发布阻塞项", None)
.await?;
println!("{}", result.text);
println!("{}", result.usage.total_tokens);
println!("{:?}", result.verification_summary().status);

流式输出

Rust
Node.js
Python
Go
Rust
use a3s_code_core::{AgentEvent, CodeError};
let (mut events, lifecycle) = session
.stream("运行相关测试并解释失败原因", None)
.await?;
while let Some(event) = events.recv().await {
match event {
AgentEvent::TextDelta { text } => print!("{text}"),
AgentEvent::ToolStart { name, .. } => println!("\n工具:{name}"),
AgentEvent::End { .. } => break,
AgentEvent::Error { message } => return Err(CodeError::Llm(message)),
_ => {}
}
}
lifecycle
.await
.map_err(|error| CodeError::Internal(error.into()))?;

每个 SDK 事件都是 EventEnvelopeV1 投影,包含 version === 1、开放的 type 字符串、完整 payload 与可选 metadata。text、toolName 等便捷字段 由信封统一派生。消费端应保留默认分支,并为未来的事件类型保存原始载荷。

调整或中断活动 Run

Run Control 修改正在执行的操作,不会启动第二个对话 Turn。steer 把更新后的用户指令 排队;在下一次服务提供商调用时,它作为 user.message 追加到事实日志并随该请求发送。 interrupt 协作式取消当前 Provider 与工具工作,并等待受监督清理 完成后再把 Run 置为 cancelled。

Rust
Node.js
Python
Go
Rust
use a3s_code_core::{InterruptRequest, SteerRequest};
let state = session.run_control_snapshot().await;
let receipt = session
.steer(SteerRequest::new("优先处理失败的测试"))
.await?;
println!("{:?}", receipt.state);
session
.interrupt(InterruptRequest::new().with_reason("用户停止运行"))
.await?;

调用方使用相同 Request ID 和相同载荷重试时,请求具有幂等性。可选的 Run 与预期 Turn 字段会让过期界面操作以失败为默认。已接受不等于已经应用;需要区分时,应观察 run_control_applied 或查询持久 Run 事件。两种操作都不会改变模型、权限、沙箱、预算 或确认策略。

临时提问

SDK 没有专用的临时提问辅助方法。向 send 或 stream 显式传入历史时, session.history() 不会改变,但提示和回答仍会追加到该会话的事实日志中, 同一会话的后续轮次可以看到它们。显式历史只在日志仍为空时用于初始化日志。 如果问题不能影响当前会话,请在单独的会话中提问。

TypeScript
const snapshot = session.history();
const answer = await session.send('这个 session 已经看过哪些文件?', snapshot);
console.log(answer.text);
console.log(session.history().length === snapshot.length);

恢复会话

Rust
Node.js
Python
Go
Rust
use a3s_code_core::{Agent, SessionOptions};
let session = agent
.resume_session_async(
"release-review",
SessionOptions::new().with_file_session_store("./.a3s/sessions"),
)
.await?;

使用会话存储时设置 autoSave: true,或显式调用 await session.save()。 这里恢复的是已保存的会话快照;中断运行的检查点通过 session.resumeRun(runId) 恢复。Go 使用 Save、ResumeSession 和 ResumeRun(ctx, runID) 对应同样的两层持久化。参见 持久化。

如果该运行有检查点,resumeRun 会用检查点中的对话记录重建会话的事实日志。之前的 模型回合记录为事实,不会再次发送给模型。没有检查点时,它折叠已有日志。两种情况下 都由折叠选择下一步:结果缺失的工具调用执行一次,静止的日志以零步恢复。既没有 检查点也没有日志时,调用会失败,并指出缺少会话存储或检查点。

生命周期与关闭

session.close() 是一次完整的优雅停止。首次调用会把会话切换到 已关闭状态——之后的 send/stream 调用会以 CodeError::SessionClosed 快速失败,而不会启动新运行——然后取消正在执行的运行、所有正在进行的委派子 智能体任务,以及所有挂起的人工确认。后续调用不会重复操作,且保证 不会触发 panic。用 session.isClosed()(Node)、session.is_closed()(Python) 或 session.IsClosed(ctx)(Go)查询关闭状态。

TypeScript
session.close();
if (session.isClosed()) {
// send/stream 现在会以 CodeError::SessionClosed 拒绝
}
Python
session.close()
if session.is_closed():
# send/stream 现在会以 CodeError::SessionClosed 拒绝
pass
Go
if err := session.Close(ctx); err != nil {
return err
}
closed, err := session.IsClosed(ctx)

取消令牌

每个运行都通过 child_token() 从同一个会话级父令牌派生出自己的 逐操作取消令牌,因此 close() 会一次性级联到所有正在进行的工作。需要原始 令牌的嵌入方——例如把它接入宿主侧的 select!,或者绕过 close() 的 运行存储和钩子副作用直接中止会话——可以通过 AgentSession::session_cancel_token() 克隆它。

智能体侧会话注册表

所属的 Agent 通过 Weak 引用跟踪它的存活会话(惰性回收),这样控制面 就能在不持有会话句柄的情况下驱动生命周期:

  • Agent::list_sessions() 返回存活的会话 ID(已排序,稳定)。
  • Agent::close_session(id) 按 ID 关闭单个会话——与 AgentSession::close() 相同的清理流程,从带外调用。
  • Agent::close() 关闭每个存活会话并拆除智能体持有的后台资源(同时 断开全局 MCP 连接)。返回后,新的 session / resumeSession 调用会以 CodeError::SessionClosed 快速失败。
  • Agent::is_closed() 报告智能体自身是否已被关闭。
TypeScript
const ids = await agent.listSessions();
await agent.closeSession(ids[0]);
await agent.close(); // 关闭所有剩余会话和全局 MCP
console.log(agent.isClosed());
Python
ids = agent.list_sessions()
agent.close_session(ids[0])
agent.close() # 关闭所有剩余会话和全局 MCP
print(agent.is_closed())
Go
ids, err := agent.ListSessions(ctx)
if err == nil && len(ids) > 0 {
_, err = agent.CloseSession(ctx, ids[0])
}
err = agent.Close(ctx)

宿主身份标签

SessionOptions 携带四个不透明的身份字段,宿主可以在创建会话时附加。 框架只负责传递它们——从不解释或强制执行。它们会被传播进 SessionData、 钩子和追踪,并在恢复时还原,因此宿主可以据此驱动多租户聚合、计费 和分布式追踪:

Node(驼峰命名)Python(蛇形命名)Go含义
tenantIdtenant_idTenantID多租户标签
principalprincipalPrincipal触发会话的用户或服务
agentTemplateIdagent_template_idAgentTemplateID会话实例化所基于的智能体模板或定义
correlationIdcorrelation_idCorrelationID分布式追踪关联标识
TypeScript
const session = agent.session('/repo', {
tenantId: 'tenant-example',
principal: 'principal-example',
agentTemplateId: 'agent-template-example',
correlationId: 'trace-example',
});
Python
opts = SessionOptions()
opts.tenant_id = 'tenant-example'
opts.principal = 'principal-example'
opts.agent_template_id = 'agent-template-example'
opts.correlation_id = 'trace-example'
session = agent.session('/repo', opts)
print(session.tenant_id, session.principal)
Go
session, err := agent.Session(ctx, "/repo", &code.SessionOptions{
TenantID: "tenant-example",
Principal: "principal-example",
AgentTemplateID: "agent-template-example",
CorrelationID: "trace-example",
})

运行记录与回放

每次 send() 或 stream() 都会创建运行记录。应用可以用这些记录实现界面状态、 审计、回放、取消和测试断言:

TypeScript
const runs = await session.runs();
const latest = runs.at(-1);
if (latest) {
console.log(await session.runSnapshot(latest.id));
console.log(await session.runEvents(latest.id));
}
Go
runs, err := session.Runs(ctx)
if err == nil && len(runs) > 0 {
latest := runs[len(runs)-1]
snapshot, snapshotErr := session.RunSnapshot(ctx, latest.ID)
events, eventsErr := session.RunEvents(ctx, latest.ID)
_, _, _ = snapshot, snapshotErr, eventsErr
_ = events
}

currentRun() 用来读取调用当下的当前运行。send() 或 stream() 仍在 执行时,可以把它的 id 传给 cancelRun(id) 请求取消。空闲时,currentRun() 可能返回 null,也可能保留一个运行快照;已完成历史应使用 runs(), 取消前必须检查 status:

TypeScript
const terminal = new Set(['completed', 'failed', 'cancelled']);
const current = await session.currentRun();
if (current?.id && !terminal.has(current.status)) {
await session.cancelRun(current.id);
}

智能体定义

sessionForAgent() 应用一个命名的智能体定义,只在内置智能体和传入的 agentDirs 中查找;它不会自行扫描 .a3s/agents。

TypeScript
const session = agent.sessionForAgent('/repo', 'explore', ['./agents'], {
planningMode: 'auto',
});
Go
session, err := agent.SessionForAgent(
ctx,
"/repo",
"explore",
[]string{"./agents"},
&code.SessionOptions{PlanningMode: code.PlanningAuto},
)

对于通过值定义的一次性 worker,可使用 SessionForWorker;两种方法都返回通用的 Go Session API。