Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 27 additions & 1 deletion apps/docs/content/docs/cn/code/api-contract.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,8 @@ iteration。

## Direct Tools

> 完整指南:[工具](/cn/docs/code/tools)。

已验证的宿主侧直接工具调用:

```ts
Expand Down Expand Up @@ -230,6 +232,8 @@ tool。`program` 不会出现在它自己的默认 tool set 里。

## Verification

> 完整指南:[验证](/cn/docs/code/verification)。

验证信息是 session 级能力:

```ts
Expand All @@ -247,6 +251,8 @@ console.log(formatVerificationSummary(session.verificationSummary()));

## Memory

> 完整指南:[记忆](/cn/docs/code/memory)。

Node memory 已用 `FileMemoryStore` 验证:

```ts
Expand All @@ -267,6 +273,8 @@ await session.recallByTags(['grep'], 10);

## Skills

> 完整指南:[Skills](/cn/docs/code/skills)。

文件型和 inline skills 已通过 `search_skills` 验证:

```ts
Expand All @@ -290,6 +298,8 @@ skill-file 检查使用带 YAML frontmatter 的 Markdown,并覆盖了

## Side Questions

> 完整指南:[会话](/cn/docs/code/sessions)。

`btw()` 提出一次只读临时问题,并返回独立结果:

```ts
Expand All @@ -301,6 +311,8 @@ console.log(side.totalTokens);

## Runs And Cancellation

> 完整指南:[会话](/cn/docs/code/sessions)。

每次 `send()` 或 `stream()` 都会记录可回放的 run state:

```ts
Expand All @@ -325,6 +337,8 @@ console.log(session.traceEvents());

## Persistence

> 完整指南:[持久化](/cn/docs/code/persistence)与[会话](/cn/docs/code/sessions)。

文件型 session persistence 已验证稳定 `sessionId`、`autoSave`、显式
`save()` 和 `resumeSession()`:

Expand Down Expand Up @@ -357,6 +371,8 @@ await agent.close(); // 关闭所有活 session + 断开全

## Delegation

> 完整指南:[任务](/cn/docs/code/tasks)与[编排](/cn/docs/code/orchestration)。

已验证核心委派工具的直接 helper:

```ts
Expand All @@ -377,6 +393,8 @@ await session.tasks([

## Hooks

> 完整指南:[Hooks](/cn/docs/code/hooks)。

已验证的 hook 管理面:

```ts
Expand All @@ -396,6 +414,8 @@ session.unregisterHook('docs-observer');

## Slash Commands

> 完整指南:[命令](/cn/docs/code/commands)。

自定义 slash command 通过 `session.send()` 触发:

```ts
Expand All @@ -410,6 +430,8 @@ console.log(result.text);

## Lane Queue

> 完整指南:[Lane 队列](/cn/docs/code/lane-queue)。

Queue infrastructure 是显式 opt-in:

```ts
Expand All @@ -430,6 +452,8 @@ await queued.deadLetters();

## MCP

> 完整指南:[MCP](/cn/docs/code/mcp)。闲置断开见[集群扩展点](/cn/docs/code/cluster-extension-points)。

集成检查覆盖一个真实 stdio MCP server:

```ts
Expand Down Expand Up @@ -465,7 +489,9 @@ new UnixSocketTransport('/tmp/a3s.sock').kind; // 'unix_socket'

## 集群级扩展点

这些契约让集群控制面(例如 书安OS)在**不 fork 框架**的前提下接入多租户、成本管控和容错运行。框架定义"决策点"和"结构化事件",**策略实现由 host 提供**。
> 完整指南:[集群扩展点](/cn/docs/code/cluster-extension-points)(身份标签、预算守卫、集群事件、确定性 ID/回放、loop checkpoint、保留上限)。

这些契约让集群控制面在**不 fork 框架**的前提下接入多租户、成本管控和容错运行。框架定义"决策点"和"结构化事件",**策略实现由 host 提供**。

### 身份标签

Expand Down
188 changes: 188 additions & 0 deletions apps/docs/content/docs/cn/code/cluster-extension-points.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
---
title: "集群扩展点"
description: "集群宿主用来在多节点上运行长时会话、且无需分叉框架的接缝。"
---

import { Tab, Tabs } from 'fumadocs-ui/components/tabs';

# 集群扩展点

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

本页会明确区分哪些接缝在两个 SDK 中都可用(以 Node.js + Python 代码展示),哪些是当前在 Rust 核心中配置的(以散文描述,SDK 接入随后跟进)。

## 身份标签

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

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

<Tabs groupId="lang" items={['Node.js', 'Python']}>
<Tab value="Node.js">

```ts
const session = agent.session('/path/to/project', {
tenantId: 'acme-corp',
principal: 'user:42',
agentTemplateId: 'reviewer-v3',
correlationId: 'req-9f2c',
});

// Getters return string | null
console.log(session.tenantId); // 'acme-corp'
console.log(session.principal); // 'user:42'
console.log(session.agentTemplateId); // 'reviewer-v3'
console.log(session.correlationId); // 'req-9f2c'
```

</Tab>
<Tab value="Python">

```python
opts = SessionOptions()
opts.tenant_id = 'acme-corp'
opts.principal = 'user:42'
opts.agent_template_id = 'reviewer-v3'
opts.correlation_id = 'req-9f2c'
session = agent.session('/path/to/project', opts)

# Getters are methods, return str | None
print(session.tenant_id()) # 'acme-corp'
print(session.principal()) # 'user:42'
print(session.agent_template_id()) # 'reviewer-v3'
print(session.correlation_id()) # 'req-9f2c'
```

</Tab>
</Tabs>

## 预算 / 成本守卫

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

<Tabs groupId="lang" items={['Node.js', 'Python']}>
<Tab value="Node.js">

```ts
session.setBudgetGuard({
checkBeforeLlm(sessionId, estimatedTokens) {
if (overLimit(sessionId, estimatedTokens)) {
return { decision: 'deny', resource: 'tokens', reason: 'monthly cap reached' };
}
return { decision: 'allow' };
},
recordAfterLlm(sessionId, usage) {
meter(sessionId, usage);
},
});

// Clear the guard
session.setBudgetGuard(null);
```

守卫回调**绝不能抛出异常**——抛出的错误会被视为放行(Allow)。

</Tab>
<Tab value="Python">

```python
class MyGuard:
def check_before_llm(self, session_id, estimated_tokens):
if over_limit(session_id, estimated_tokens):
return {'decision': 'deny', 'resource': 'tokens', 'reason': 'monthly cap reached'}
return {'decision': 'allow'}

def record_after_llm(self, session_id, usage):
meter(session_id, usage)

opts = SessionOptions()
opts.budget_guard = MyGuard()
session = agent.session('/path/to/project', opts)

# To clear: set opts.budget_guard = None and re-create the session.
```

</Tab>
</Tabs>

两个 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)。 |

这种健壮性是刻意为之的:**缺失的守卫方法**会被当作宽松默认值处理,而**回调出错则回退为放行(Allow)**。行为异常的守卫永远无法中止一个活动会话——只有显式的 `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`(参见[钩子](/cn/docs/code/hooks))。请将上述三个变体视为已记录在案的契约;宿主负责通过其钩子执行器发出它们。

## 确定性 ID 与时间(重放)

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

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

## 循环检查点与运行恢复

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

<Tabs groupId="lang" items={['Node.js', 'Python']}>
<Tab value="Node.js">

```ts
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');
```

</Tab>
<Tab value="Python">

```python
from a3s_code import FileSessionStore

opts = SessionOptions()
opts.session_store = FileSessionStore('./.a3s/sessions')
opts.session_id = 'session-from-node-a'
session = agent.session(workspace, opts)

result = session.resume_run('run-id-from-node-a')
```

</Tab>
</Tabs>

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

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

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

## 长时运行会话的保留上限

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

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

这是当前通过 Rust 核心的 `SessionRetentionLimits` 配置的;SDK 形态将在后续跟进落地,因此目前没有对应的 Node/Python 代码。关于已经存在于 SDK 面上的每会话资源上限,参见[限制](/cn/docs/code/limits)。

---

**另见:** [多机部署](/cn/docs/code/multi-machine) · [持久化](/cn/docs/code/persistence) · [限制](/cn/docs/code/limits) · [钩子](/cn/docs/code/hooks)
74 changes: 69 additions & 5 deletions apps/docs/content/docs/cn/code/examples/ahp-safety.mdx
Original file line number Diff line number Diff line change
@@ -1,17 +1,81 @@
---
title: "AHP 安全"
description: "为 session 连接外部驾驭层"
title: "AHP 传输与安全"
description: "通过 AHP 传输将会话连接到外部宿主,并使用安全提供器与权限策略对其行为进行管控。"
---

# AHP 安全
import { Tab, Tabs } from 'fumadocs-ui/components/tabs';

# AHP 传输与安全

A3S Code 会话可以通过可插拔的传输层(AHP,即 Agent Host Protocol,智能体宿主协议)连接到外部宿主。无论由哪种传输承载消息,安全层都是独立的:**安全提供器**会审查工具调用,而**权限策略**决定哪些操作可以在无人参与的情况下执行。当你需要一个由远程宿主驱动、但仍强制执行安全默认姿态的会话时,请参考本页。

传输层与安全层互相正交。你可以把 `HttpTransport` 换成 WebSocket 或 Unix 套接字传输,而无需改动权限规则,反之亦然。

<Tabs groupId="lang" items={['Node.js', 'Python']}>
<Tab value="Node.js">

```ts
import { Agent, HttpTransport } from '@a3s-lab/code';
import { Agent, HttpTransport, DefaultSecurityProvider } from '@a3s-lab/code';

const agent = await Agent.create('agent.acl');

const session = agent.session('/repo', {
// AHP transport: how the session reaches its external host.
ahpTransport: new HttpTransport('http://localhost:8080/ahp', process.env.AHP_TOKEN),
// Safety: the security provider vets each tool call; the permission
// policy decides what runs unattended. These apply regardless of transport.
securityProvider: new DefaultSecurityProvider(),
permissionPolicy: { defaultDecision: 'ask' },
});

const result = await session.run('Audit the repo for hardcoded secrets.');
console.log(result);

await session.close();
```

其他传输遵循相同的写法——`WebSocketTransport`、`UnixSocketTransport` 以及进程内的 `StdioTransport` 在这里都可以互换。将 `defaultDecision` 设为 `'deny'` 可获得更严格、完全受控的姿态;仅在受信任的沙箱环境中才使用 `'allow'`。

</Tab>
<Tab value="Python">

```python
import os

from a3s_code import (
Agent,
SessionOptions,
HttpTransport,
DefaultSecurityProvider,
PermissionPolicy,
)

agent = Agent.create(open("agent.acl").read())

opts = SessionOptions()
# AHP transport: how the session reaches its external host.
opts.transport = HttpTransport("http://localhost:8080/ahp", os.environ["AHP_TOKEN"])
# Safety: the security provider vets each tool call; the permission
# policy decides what runs unattended. These apply regardless of transport.
opts.security_provider = DefaultSecurityProvider()
opts.permission_policy = PermissionPolicy(default_decision="ask")

session = agent.session("/repo", opts)

result = session.run("Audit the repo for hardcoded secrets.")
print(result)

session.close()
```

这个示例展示 session option 形状和 transport constructor。依赖策略、上下文注入、idle、审计或监督行为前,应先测试你的 live AHP server。
将 `default_decision` 设为 `"deny"` 可获得更严格、完全受控的姿态;仅在受信任的沙箱环境中才使用 `"allow"`。传输层与安全层互相独立。

</Tab>
</Tabs>

## 说明

- **AHP 是传输层,而非策略。** 选择 HTTP、WebSocket 还是 Unix 套接字,改变的是会话*如何*抵达其宿主,而不改变智能体*被允许做什么*。安全完全由安全提供器和权限策略掌控。
- **`DefaultSecurityProvider`** 提供了合理的基线。当你需要强制执行组织特定的规则(路径白名单、命令审查、脱敏)时,请提供你自己的提供器。
- **权限策略**(`defaultDecision` / `default_decision`)是最重要的单个安全开关。交互式使用时优先选择 `'ask'`,无人值守运行时除非工作区已沙箱化,否则优先选择 `'deny'`。
- 这个示例验证会话选项的形状和传输构造函数。在依赖策略、上下文注入、idle、审计或监督行为之前,应先测试你的实时 AHP 服务器。
Loading