@tansr/serve
v0.12.0
Published
tansr Agent 会话引擎 (tansr Agent Session Engine): the bare /v2 agent-session protocol engine (REST + SSE) you embed in your own Node service. Session auth is fully yours via the authenticate seam. This package is not a gateway — auth policy, rate limiting, q
Downloads
1,820
Readme
tansr Agent 会话引擎(@tansr/serve)
对外发布名 =
@tansr/serve(用户拍板 2026-09-01)。workspace 内恒以@tansr/server引用;发布 tarball 由pnpm release:pack-serve盖发布名。 license = MIT(同批拍板;正式协议考察待全部开发完成后进行)。
定名(doc/118 §八 G1,2026-09-03 用户授权代拍):本包是 tansr Agent 会话引擎
——多租 Agent 会话的协议引擎(/v2 REST + SSE)+ 内置真平台装配(A 案,用户
拍板 2026-09-01),以 npm 包形态嵌进你自己的 Node 服务。本包不是网关:
「网关」一词在 tansr 体系内只指 tansr-api,鉴权策略/限流/配额/计费归开发者的登录态
体系与平台侧;引擎自带的准入帽、rateLimit 缝与上游治理层是自保护面,不是替你
做网关。wire 契约权威 = doc/98(Agent 会话服务 v2 契约冻结件)。
引擎负责传输与协议机器:11 个 /v2 端点路由、SSE 编帧与逐会话事件环形缓冲、
Last-Event-ID 重放、三类桥回执受理(工具/权限/提问)、多会话治理(保留窗/
闲置回收/并发帽)、轮末出站 webhook 通知。会话鉴权完全归你:引擎只暴露
authenticate 注入缝,token 形制/签发/校验由你的登录态体系全权自定
(最大放权)——引擎恒不规定、恒不内置任何 token 服务。
真装配内置:createAgentSessionFactory({ platform }) 四行接通 tansr
平台——appid/appkey → per-endUser app_user 令牌(POST /v1/app-tokens,
TWP 签名)→ bundle registry 模型装配 → 内核查询环真调模型,计费按 endUser
归因;不用自写 Agent、不依赖任何 tansr 私有仓。
安装
npm install @tansr/serve运行时要求:Node ≥ 22.19。唯一第三方依赖 zod 随包解析;tansr 内部包
(@tansr/protocol、@tansr/i18n、@tansr/kernel、@tansr/providers、
@tansr/sdk)已编译内联,恒不外泄安装面。
系统媒体工具 / System media tools
图像生成、视频生成、语音转文字和文字转语音均由内核系统工具执行,工具名为
ImageGen、VideoGen、SpeechToText、TextToSpeech。平台内置会话工厂根据应用能力
装配它们,并使用 SDK 的平台提供方调用授权模型;capabilities.platform 表示平台服务
授权,不表示独立的“平台工具”类别。平台令牌、模型权限、配额、计费及运行时权限继续生效。
Android/iOS 经 /v2 会话连接时,模型与系统媒体工具运行在开发者的 serve 进程中,
手机负责提交输入和展示真实工具事件/媒体产物,不因此取得服务端文件或终端命令权限。
用户主动录音转写、朗读使用既有音频直连端点;它们与模型调用媒体工具并存,不自动进入聊天历史。
SDK 进程内集成仍须通过 tools.builtin 显式选择四媒体;这与 serve 平台工厂按应用授权装配的入口不同。
Image generation, video generation, transcription, and speech synthesis are built-in system tools.
The platform session factory assembles the authorized tools with platform-backed providers.
capabilities.platform retains its service-authorization meaning. On Android and iOS, these tools
run in the serve process; the mobile client renders their events and artifacts. User-triggered audio
operations use the separate direct endpoints and do not fabricate model tool events or chat history.
会话元数据与开发者登录
新构建的 GET /v2/sessions/:id 可返回可选的 meta.context 和 meta.media。
前者区分下一轮/执行轮模型、真实观测用量及本地上下文预算;后者列出四类媒体的应用授权、
模型与限制、采样时间。字段缺席表示未知,客户端不能拿累计 token 用量当作当前上下文占用。
媒体快照是会话装配时的授权目录,实际工具是否装配及执行权限仍以服务端执行为准。
这次没有增加 HTTP 切模端点,也没有修改已冻结的内核事件协议。
countSpeechCharacters、planSpeechInput 和相关类型从本包公开导出,用于按授权模型的
maxChars 校验或显式分段朗读;示例遇到尚无新出口的已发布包时会提示升级。
开发者登录服务与平台 app_user 分离:客户端向开发者的 login/refresh 入口取短效访问票,
serve 的 authenticate 验证该票并返回 endUserId,平台 appkey 始终只留在服务端。
完整远程示例见 examples/serve-demo;有界续期不会自动重放音频或聊天 POST。
New builds may include optional meta.context and meta.media in session metadata. Missing fields
mean unknown. Context estimates and observed billing usage are separate; the media catalog reflects
the session's assembly-time authorization. Developer login and refresh credentials remain separate
from platform app_user tokens. The public speech planner uses the authorized model's limit and explicit
segmentation; authentication recovery never automatically replays paid POST requests.
集成骨架
import { createAgentSessionFactory, registerBuiltinLocales, startServer } from '@tansr/serve';
import type { AgentSessionsOptions } from '@tansr/serve';
registerBuiltinLocales(); // 可选:错误体文案本地化(不注册则回落键名)
// ② 真装配(内置,A 案):appid/appkey → per-endUser 令牌 → 内核查询环。
// 也可换成你自实现的 AgentSessionFactory(注入形,契约同形)。
const build = createAgentSessionFactory({
platform: {
apiBaseUrl: 'https://api.tansr.com',
appId: process.env.TANSR_APP_KEY_ID!, // 平台控制台签发
appKey: process.env.TANSR_APP_KEY!, // 恒不下发端、恒不入日志
},
cwd: process.cwd(),
});
const v2: AgentSessionsOptions = {
// ① 鉴权缝(唯一鉴权面):你的登录态 → endUserId;null = 401。
// token 形制自定——自家 JWT/session cookie/OAuth/API key 皆可。
authenticate: async (req) => {
const user = await myAuth.verify(req.headers['authorization']);
return user ? { endUserId: user.id } : null;
},
createSession: build.factory,
// ③ 可选:SessionStore 读面(resume/列表)、治理参数、轮末 webhook 缝
// store, governance, onTurnEndNotify, ...
};
const server = await startServer({
port: 8787,
host: '127.0.0.1',
token: myV1Token, // /v1 面 Bearer(与 /v2 鉴权互不相通)
createSession: myV1Factory,
version: '1.0.0',
v2,
});平台内置形工具面按应用 bundle 有效能力档按位装配(doc/112:TodoWrite /
AskUser / WebSearch / ImageGen / VideoGen 逐位随控制台「能力面」开关;逐会话远程
工具三桥受 customTools 位——位关而申报 clientTools 即 400 明告);应用平台类型
非「移动应用」时经 onPlatformWarning 与 server.platform.warning 帧提示
app_platform_mismatch(不拒)。profile 请求位不支持(带上会 400 明告)。
一体式可跑示例见仓内 examples/serve-demo/agent-real.ts。
三层鉴权定盘(架构拍板 2026-08-31)
| 层 | 凭据 | 归属 |
|---|---|---|
| sdk ↔ serve(会话面) | 你自定的登录态/token | 完全开发者侧;引擎只经 authenticate 缝取 endUserId |
| serve ↔ api(引擎上游) | appid + appkey(平台「应用」颁发) | serve 持有;以 appkey 换 per-endUser app_user 令牌供内部 harness 调模型,endUser 计费归因 |
| sdk 持 appid | 公开应用标识(client_id 语义) | 可下发端亮明身份;恒非凭据 |
红线:appkey 恒不下发端;app_user 令牌恒不出 serve;/v2 恒不受理 /v1
Bearer。会话 id 恒非鉴权因子(逐端点归属校验独立成立,跨租 403/404)。
推送唤醒 webhook 缝(轮末出站通知;doc/98 §五-5.4,A4-2/D5)
移动端 App 退后台、SSE 断开后,轮跑完了怎么唤醒用户?引擎恒不内置推送
通道(FCM/厂商通道由你自接,D5 终拍:SDK 与 serve 恒不内置),只留一条
webhook 缝:AgentSessionsOptions.onTurnEndNotify 在场时,会话轮终局
(turn.completed / turn.aborted)且该刻无活跃 SSE 订阅者才向你的 URL
POST 一份最小载荷(有订阅者 = 客户端在线,恒不发;同轮恒一发,防抖)。
const v2: AgentSessionsOptions = {
// ...authenticate / createSession...
onTurnEndNotify: {
url: 'https://your-service.example/tansr/turn-end',
secret: process.env.NOTIFY_SECRET, // 可选:HMAC-SHA256 对原始体全文签名
},
};- 载荷(
TurnEndNotifyPayload冻结形,恒不携消息内容恒不携凭据):{ sessionId, endUserId, turnId?, status: 'completed'|'aborted', reason?, lastSeq, ts }——lastSeq供客户端持水位比对判断是否需要重放追赶,ts供弃过期/防重放;reason?(contract-v0.21 / RFC-SC-1)= 终局帧TerminalReason原值,单帧区分client_gone(订阅者离场被策略止损)/internal_error(内核故障)/ 用户中断aborted_*等,接收方按「已知值专项 + 未知值兜底」消费; - 签名:
secret在场时携x-tansr-signature: v2=<hex>, v1=<hex>(TURN_END_NOTIFY_SIGNATURE_HEADER;FX-C-43 双签一版,signatureVersions缺省['v2','v1']):v2= protocolWEBHOOK_SIGNATURE_V2canonical(随行x-tansr-timestamp/x-tansr-nonce),v1= 对原始请求体全文 HMAC-SHA256—— 接收端先验签再消费(tansrd 缺省只收 v2); - CLI 形态两键(FX-C-09/续):
tansr serve --v2以TANSR_SERVE_NOTIFY_URL(绝对 http(s) URL)/TANSR_SERVE_NOTIFY_SECRET两 env 接同一缝(见下文旋钮表notify组)——无 URL = 不出站;secret 单独在场忽略并告警;启动期 stdout 一行 只打 URL 的 origin 与「签名在场 / 缺席」,secret 与路径 / 查询串恒不打印;未开--v2时两键列入「仅对 /v2 宿主生效」告警; - 投递纪律:fire-and-forget 恒不阻断会话主链;2xx 即成功,否则指数退避
重试(缺省 1+2 次,
V2_LIMITS.notifyMaxRetries/notifyTimeoutMs/notifyBackoffMs可覆写);终败走onDeliveryFailure结构化通报;服务器优雅关闭期恒不出站; - 收到通知后:你的服务自行走 FCM/厂商推送叫醒 App;App 回连
GET /v2/sessions/:id/events带Last-Event-ID断点续传,窗口外走 history/resume 恢复链。
主要出口
startServer(options)/StartServerOptions(v2?: AgentSessionsOptions; v2 缺席 =/v2面零暴露;v1?: V1SessionsOptions=/v1终结记录两级逐出governance.{retentionMs, maxRetainedSessions, sweepIntervalMs},缺席 = 不逐出 ——库形态终结会话永驻可重放,长驻宿主建议 30 min / 10 000,CLItansr serve即此缺省);AgentSessionsOptions:authenticate(唯一鉴权缝)、createSession(AgentSessionFactory)、store(AgentStoreReader)、governance、mediaMaxBodyBytes、onTurnEndNotify;- 真装配面(A 案,2026-09-01):
createAgentSessionFactory(platform内置形 /factory注入形)、createPlatformServeSessionFactory、createAppTokenMinter/createPlatformSessionAssembler(铸令牌与 per-endUser 装配缝)、createServeAgentSessionStore(轮粒度持久 store)、ServeSessionDriver/AgentSessionBridge/buildRemoteTool及契约类型族 (ServeSessionFactory/ServeSessionExtras/AssembledSessionModel等); - 契约持份:
V2_LIMITS/V2_ERROR_CODE/CONTROL_FRAME/AGENT_SESSION_CONTRACT_VERSION/ 请求体 zod schema 族 / 控制帧载荷类型; - SSE 机器:
encodeSseFrame/encodeAgentStreamFrame/EventRingBuffer; - 事件日志接口缝(SC-35):
SessionEventLog/SessionEventLogRead/SessionEventLogFactory/MemorySessionEventLog(缺省实现),经AgentSessionsOptions.eventLog注入(见 Operations「扩展档位」); - 轮末通知:
TurnEndNotifier/TURN_END_NOTIFY_SIGNATURE_HEADER; - i18n 便携面:
registerBuiltinLocales/setLocale/negotiateLocale。
发布形态
- 双态 exports:开发态
src/index.ts(workspace tsx 直消费);发布态publishConfig三键覆盖指dist(pnpm pack应用); build.mjs(S-D1 内联范式):esbuild 内联@tansr/protocol/@tansr/i18n/@tansr/kernel/@tansr/providers/@tansr/sdk→dist/index.js(含 createRequire 垫片治内联 CJS 依赖);tsc + rollup-plugin-dts 卷积 →dist/index.d.ts;产物守卫拒任何@tansr/*残留;pnpm release:pack-serve:pack 窗口盖发布名@tansr/serve+ 剔内部 devDeps,打包后守卫复检 manifest/依赖面/文件表/license(MIT + LICENSE 件 版权行 Tansr)+ ⑥真装配出口断言(动态 import 验三出口可调 + wire/d.ts 锚);发布动作恒候用户口令。
宿主信号接线(Graceful shutdown)
@tansr/serve 以 npm 包嵌进你的 Node 进程,进程信号归宿主:引擎恒不在
process 上注册信号监听器(嵌入库不替宿主决定进程何时退出),也就不会替你
处理 SIGTERM/SIGINT——宿主必须自己接线。不接线的后果:编排器常规停机
(docker stop / systemd / Kubernetes 缺省都发 SIGTERM)= 进程被硬杀,全部
SSE 连接 reset、在飞轮丢、无收口日志。
startServer 返回的 server.drain({ timeoutMs })(幂等;缺省 30 s)按序做四件事:
① 拒新——readiness 翻红(server.readiness() 供你的 /readyz 读)、新建/resume
一律 503 draining 携 Retry-After、全部在场 SSE 下发一帧 retry: 长间隔
(sse.drainRetryMs,缺省 10 s,客户端换台后再连);② 等在飞轮——/v2 运行中的
轮至多等 timeoutMs 到终态,到点剩余 interrupt()(轮末 store 提交照走,resume 可
续);③ flush 落盘——等全部 fire-and-forget 的 store 提交/建行 settle
(createAgentSessionFactory 的 flush);④ 停机——终结会话(有界:在飞 pump 至多等
closeAllTimeoutMs,缺省 5 s、Infinity 无界;到点记 server.close_timeout{face,pending,timeoutMs}
后照常继续,永不 settle 的 pump 不再能挂住停机;RF-03)、停监听、掐空闲连接、
有界等待在飞非 SSE 请求答完(≤ 5 s)再掐残余连接。回执
{ completedTurns, abortedTurns, flushedCommits, durationMs } 可入日志。
server.close() 保持硬收口语义(立即中止在飞轮、不等落盘;drain 进行中调用则等
drain 完成);/v1 会话无粗态读面,drain 不等其在飞轮。
最小接线——两枚信号共用一个 handler,幂等位防重复进入;第二枚信号不要吞掉,应立即硬退 (运维按两次 Ctrl+C / 两次 SIGTERM 强停的习惯,吞掉 = 只能等编排器 SIGKILL):
let stopping = false;
const shutdown = (signal: NodeJS.Signals): void => {
if (stopping) {
console.error(`[my-service] ${signal} received again, exiting now`);
process.exit(2);
}
stopping = true;
console.error(`[my-service] ${signal} received, draining...`);
server
.drain({ timeoutMs: 30_000 })
.then((report) => {
console.error(`[my-service] drained`, report);
process.exit(0);
})
.catch((error: unknown) => {
console.error('[my-service] shutdown failed', error);
process.exit(1);
});
};
process.on('SIGTERM', shutdown);
process.on('SIGINT', shutdown);要点:
- 宽限期 ≥ drain 超时 + 收尾余量:
drain({ timeoutMs: 30_000 })最坏等 30 s 在飞轮- ≤ 5 s 被中止轮收口 + ≤ 5 s 连接收口;容器
stop_grace_period(compose 缺省 10 s)、KubernetesterminationGracePeriodSeconds、systemdTimeoutStopSec部署件统一 45 s(doc/118 §八 G10;deploy/serve-v2/与deploy/serve/已写),或把timeoutMs调小并同步调小宽限。宽限到点编排器 SIGKILL,等价硬杀;
- ≤ 5 s 被中止轮收口 + ≤ 5 s 连接收口;容器
- 信号要能到达 node:容器
CMD/ENTRYPOINT用 exec 形(JSON 数组)或--init(tini 作 PID 1 转发),不要sh -c "node …"(sh 当 PID 1 不转发); - 退出码由你定:引擎恒不调
process.exit;示例里drain()成功exit(0)、 失败exit(1),与你的进程监督器约定一致即可;tansr serve(CLI 形态)订阅 SIGINT / SIGTERM / SIGHUP / SIGBREAK,首枚同走 drain(超时经TANSR_SERVE_DRAIN_TIMEOUT_MS可配,缺省 30 s),第二枚立即exit(2),整链 drain 超时 + 15 s 未收口亦exit(2), 两者都记process.hard_exit{signal,reason,budgetMs,elapsedMs,exitCode}(RF-03;共享组件packages/cli/src/assembly/process-lifecycle.ts);CLI 形态另把每轮墙钟缺省定为maxTurnWallClockMs = 1800000(30 min;TANSR_SERVE_TURN_WALL_CLOCK_MS=0停用;审查 #9)—— 库缺省仍不设,嵌入宿主自行决定; - Windows 注记:
process.on('SIGTERM')可注册但系统不会发出、taskkill /F不发任何信号; 控制台关闭以 SIGHUP 到达且系统只留约 10 s(CLI 形态对 SIGHUP 收口预算恒 ≤ 3 s);SIGBREAK (Ctrl+Break)与 SIGINT 同义。本机开发以 Ctrl+C(SIGINT)收束,Linux/容器上 SIGTERM / SIGHUP 都会到; - 与
onTurnEndNotify的关系:优雅关闭期出站通知恒不发(见上文投递纪律), 客户端靠回连Last-Event-ID/ history / resume 追赶。 - 进程级兜底(十王修案 FX-C-02):引擎恒不替你挂
unhandledRejection/uncaughtException监听,但导出installProcessGuards({ logger, onFatal, drain?, drainTimeoutMs? })(返回卸载函数) 供宿主入口安装一次:任何逃逸到进程级的拒绝 / 异常恒记一条结构化事件process.unhandled_rejection/process.uncaught_exception{ origin, message, stack(≤ 2 KB), policy }(旧形 logger 回落 i18n 文案行);onFatal:'drain-exit'= 记完 →drain()(有界,缺省 10 s)→exit(1)(多租常驻进程缺省:tansr serve/ serve-v2 参考宿主 / tansrd);'log'= 记完继续 (单用户入口缺省:acp / mcp serve / headless / serve-demo)。envTANSR_SERVE_ON_UNHANDLED=log|drain-exit经resolveServeRuntimeOptions(env).onUnhandled翻转;路由层错误边界(SC-01)与建会临界区 (FX-C-01)是第一道,这是最后一道——不是替代品。
完整可跑示例见仓内 examples/serve-demo/server.ts(接线封装在 examples/serve-demo/runtime.ts)。
Operations(运维面:指标 / 健康 / 结构化日志 / request-id / 旋钮 / 部署)
以下全部属运维面,不属 doc/98 wire 契约(端侧不感知;三观测端点是否入契约候
G4-g 拍板)。数字口径与 SLO 词表见 doc/report/serve并发审计-D路-可观测性与横向扩展.md §七-1。
运维旋钮:TANSR_SERVE_* 环境变量 → 库选项(SC-32)
引擎的全部运维位都是 startServer / 工厂 / governor 的库选项;进程形态(容器、CLI
tansr serve)需要一个配置入口,本包给出单一事实源 resolveServeRuntimeOptions(env)
(纯函数,不读 process.env、不写日志;表 SERVE_RUNTIME_ENV 与名单 SERVE_RUNTIME_ENV_NAMES
同出口)。宿主拿到结构化结果后各自展开:
import { createUpstreamGovernor, resolveServeRuntimeOptions, startServer } from '@tansr/serve';
const runtime = resolveServeRuntimeOptions(process.env);
for (const w of runtime.warnings) logger.error(`ignoring ${w.name}="${w.value}": expected ${w.expected}`); // 非法值:忽略,恒不拒启
const governor = createUpstreamGovernor(runtime.upstreamGovernor); // TANSR_SERVE_UPSTREAM_MAX_INFLIGHT → bulkhead
const build = createAgentSessionFactory({ cwd, logger, platform: { apiBaseUrl, appId, appKey, governor, ...runtime.session } }); // 分层超时 / 每轮墙钟
const server = await startServer({
host, port, token, createSession, logger: runtime.logFormat === 'json' ? jsonLogger : textLogger, // TANSR_SERVE_LOG_FORMAT
governor,
...runtime.server, // admission / http / sse / observability / eventBufferMaxBytes
v2: { authenticate, createSession: build.factory, store: build.storeReader,
...runtime.v2 }, // /v2 治理五键 + 分片码 sessionIdShard(TANSR_SERVE_SESSION_SHARD)
});纪律(doc/118 §八 G5/G9 代拍):任一变量缺席/空串即不落键 → 未设 = 库缺省,行为零漂移;
非法值(非整数、越界、枚举外)进 warnings 由宿主一行告警后忽略,恒不拒启;applied 列出生效
的变量名,appliedRuntimeEnvByGroup(runtime, 'governance', 'v2') 可判某些组是否在场(CLI tansr serve
缺省是 /v1 面,治理五键与分片码在场即提示无效;tansr serve --v2 / TANSR_SERVE_V2=1 起挂 /v2 单运维方形
——同一 Bearer token + x-tansr-end-user 头分域、store 落 <sessionsDir>/agent-v2/、cwdPolicy = 启动 cwd 子树,
此时这些键有消费点;FX-C-46,doc/98 §十 2026-09-08 行)。建议值与部署口径见 deploy/serve-v2/README.md §三 与
deploy/serve-v2/.env.example;全表由 runtime-options.ts 的 SERVE_RUNTIME_ENV 表生成如下(FX-C-41,
doc/130 R-12 生成律:pnpm env:example 再生、pnpm docs:check 幂等门;同一张表还生成
packages/server/.env.example,pnpm env:check 锁住;process 组的 TANSR_SERVE_ON_UNHANDLED →
installProcessGuards({ onFatal }),见上节「进程级兜底」;factory 组三键 →
createAgentSessionFactory({ ...runtime.factory }) 顶层键,语义见表后一段):
全表 31 键(键序 = 表声明序;缺席 / 空串 = 库缺省,非法值忽略并告警):
| 变量 | 组 | 目标选项 | 形态 | 约束 | 单位 |
|---|---|---|---|---|---|
| TANSR_SERVE_MAX_ACTIVE_SESSIONS | admission | admission.maxActiveSessions | int | integer >= 1 | — |
| TANSR_SERVE_MAX_SSE_CONNECTIONS | admission | admission.maxSseConnections | int | integer >= 1 | — |
| TANSR_SERVE_MAX_INFLIGHT_BODY_BYTES | admission | admission.maxInflightBodyBytes | int | integer >= 1 | bytes |
| TANSR_SERVE_ELD_THRESHOLD_MS | admission | admission.eventLoopDelayP99Ms | int | integer >= 1 | ms |
| TANSR_SERVE_REQUEST_TIMEOUT_MS | http | http.requestTimeout | int | integer >= 0 | ms |
| TANSR_SERVE_EXPOSE_OBSERVABILITY | observability | observability.expose | bool | 1|true|yes|on / 0|false|no|off | — |
| TANSR_SERVE_EXPOSE_METRICS | observability | observability.exposeMetrics | bool | 1|true|yes|on / 0|false|no|off | — |
| TANSR_SERVE_EVENT_BUFFER_MAX_BYTES | ring | eventBufferMaxBytes | int | integer >= 1 | bytes |
| TANSR_SERVE_ORPHAN_GRACE_MS | governance | v2.governance.orphanGraceMs | int | integer >= 0 | ms |
| TANSR_SERVE_IDLE_AFTER_GONE_MS | governance | v2.governance.idleAfterGoneMs | int | integer >= 0 | ms |
| TANSR_SERVE_MAX_RETAINED_SESSIONS | governance | v2.governance.maxRetainedSessions | int | integer >= 0 | — |
| TANSR_SERVE_MAX_SESSION_LIFETIME_MS | governance | v2.governance.maxSessionLifetimeMs | int | integer >= 1 | ms |
| TANSR_SERVE_MAX_TURNS_PER_SESSION | governance | v2.governance.maxTurnsPerSession | int | integer >= 1 | — |
| TANSR_SERVE_SESSION_SHARD | v2 | v2.sessionIdShard | int | integer in [0, 255] | — |
| TANSR_SERVE_NOTIFY_URL | notify | v2.onTurnEndNotify.url | string | absolute http(s) URL | — |
| TANSR_SERVE_NOTIFY_SECRET | notify | v2.onTurnEndNotify.secret | secret | non-empty string; value never logged; requires TANSR_SERVE_NOTIFY_URL | — |
| TANSR_SERVE_UPSTREAM_MAX_INFLIGHT | upstream | governor.bulkhead.maxConcurrent | int | integer >= 1 | — |
| TANSR_SERVE_UPSTREAM_CONNECT_TIMEOUT_MS | session | session.timeouts.connectTimeoutMs | int | integer >= 1 | ms |
| TANSR_SERVE_UPSTREAM_IDLE_TIMEOUT_MS | session | session.timeouts.idleTimeoutMs | int | integer >= 1 | ms |
| TANSR_SERVE_UPSTREAM_TOTAL_TIMEOUT_MS | session | session.timeouts.totalTimeoutMs | int | integer >= 1 | ms |
| TANSR_SERVE_TURN_WALL_CLOCK_MS | session | session.maxTurnWallClockMs | int | integer >= 1 | ms |
| TANSR_SERVE_HOST_SHELL | session | session.hostShell | enum | posix|powershell | — |
| TANSR_SERVE_SSE_MAX_BUFFER_BYTES | sse | sse.maxBufferBytes | int | integer >= 1 | bytes |
| TANSR_SERVE_SSE_SLOW_POLICY | sse | sse.onSlowSubscriber | enum | disconnect|drop-oldest | — |
| TANSR_SERVE_SSE_RETRY_MS | sse | sse.retryMs | int | integer >= 0 | ms |
| TANSR_SERVE_LOG_FORMAT | log | logger.log | enum | json|text | — |
| TANSR_SERVE_ON_UNHANDLED | process | processGuards.onFatal | enum | log|drain-exit | — |
| TANSR_SERVE_READY_STREAM | process | readyFrame | enum | stdout|stderr|none | — |
| TANSR_SERVE_CWD_ON_RESUME | factory | factory.cwdOnResume | enum | current|stored | — |
| TANSR_SERVE_SESSION_MAX_AGE_DAYS | factory | factory.sessions.retention.maxAgeDays | int | integer >= 1 | days |
| TANSR_SERVE_SESSION_MAX_PER_END_USER | factory | factory.sessions.retention.maxPerEndUser | int | integer >= 1 | — |
分组:admission(4) / http(1) / observability(2) / ring(1) / governance(5) / v2(1) / notify(2) / upstream(1) / session(5) / sse(3) / log(1) / process(2) / factory(3)。同表生成的部署样例:packages/server/.env.example。
factory 组三键语义(表内只列形态与约束):TANSR_SERVE_CWD_ON_RESUME=current|stored → cwdOnResume,
resume 时存储 cwd ≠ 本次的处置——配了 cwdPolicy 的宿主缺省 stored(存储值重过策略闸,
策略拒 / 目录已失 → 409 cwd_unavailable{detail.reason}),未配缺省 current;current = 一键回退,十王修案 FX-C-08;
TANSR_SERVE_SESSION_MAX_AGE_DAYS / TANSR_SERVE_SESSION_MAX_PER_END_USER → sessions.retention.{maxAgeDays,maxPerEndUser},
会话保留期(十王修案 FX-C-09 / C2-04):两者皆缺席 = 零删除 = 现状永存;任一在场即启用治理扫描第 ⑤ 段「store 保留」——
每 sweep 周期经工厂 sweepRetention 触发、实扫间隔缺省 1 h,按 store meta.updatedAt 删超龄(maxAgeDays 缺席按 30 d 兜底,
与 api 会话租约窗同值;M-05:平台内置形装配过的 endUser 域以其 bundle governance.sessionRetentionDays(控制台 org 治理配置)
覆写该域超龄阈值,缺键域沿兜底;落盘(FX-C-09 收官):该天数随建档写入会话 meta.json 的 retentionDays(resume 再装配后对账),
扫描逐会话优先按它判龄,serve 重启后零回退——收官前建的无键旧会话仍按兜底)/ 每 endUser 超数最旧的会话记录(附件 / 同居快照 / 独立根快照同删;在册活跃会话恒不删),
每轮有删除即记 session.retention_swept{scanned,deleted,failures,byReason};程序面另有 maxBytes / sweepIntervalMs
与手动 build.sweepSessionRetention({ force:true })。
不在表内(语义归各自入口):TANSR_SERVE_TOKEN(/v1 Bearer)、
TANSR_SERVE_DRAIN_TIMEOUT_MS(drain 超时,缺省 30 s)、TANSR_SERVE_V1_RETENTION_MS /
TANSR_SERVE_V1_MAX_RETAINED(SC-18)、TANSR_SERVE_MCP_CONNECTIONS(SC-31;缺省 1、上限 16。
SC-44 起只是进程级兜底层:mcpServers.<name>.pool.connections 逐台键 > 程序面 pool 选项 > 本 env
缺省 1,合并在 kernel
governance.ts单点;写了逐台键的服务器不受本 env 影响,有会话态的服务器 恒保持 1——见 doc/60 §2.5)。
部署参考件与优雅关闭时序(SC-32 / SC-32c;doc/118 §八 G1/G2/G10)
deploy/serve-v2/ 是 /v2 npm 嵌入形态的参考宿主部署件(examples/serve-demo 两入口打成单
文件镜像;compose N 副本 + nginx 分片路由/SSE 模板;K8s StatefulSet + 内层 nginx + ingress-nginx 注解 +
NetworkPolicy);deploy/serve/ 是 CLI tansr serve /v1「一容器一智能体实例」模板。要点:
- 本包不是网关:TLS 终结 / HTTP2 / IP 级限流 / WAF 归你的边缘(G10 反代前置);nginx 模板只做
SSE 直通(
proxy_buffering off、读超时 ≥ 心跳 15 s × 4、对text/event-stream不压缩、proxy_next_upstream off)与分片前缀路由,client_max_body_size 20m与引擎体帽对齐。 - 多副本 = 分片前缀路由 + 副本私有存储根(G2 先 (a) 档;SC-32b/SC-32c):会话运行态在副本内存 +
私有 store,
/v2/sessions/<id>/…必须回到创建副本。每副本持分片码v2.sessionIdShard(0–255, = 副本序号;envTANSR_SERVE_SESSION_SHARD),引擎给全新会话生成的 sessionId 前两位十六进制 恒 = 分片码(其余 30 位仍是 UUID v4 随机位,形制仍是 UUID;resume/attach 沿用原 id),反代读 id 前两位做静态map路由回创建副本;创建请求(路径无 id)任意副本轮询。客户端零改动(sessionId 对客户端恒不透明,doc/98 §五-5.5),无 cookie、无端侧头、不按 Authorization 哈希(令牌刷新不 漂移)。不能按路径 sessionId 做一致性哈希——创建请求没有 sessionId,会话生在随机副本,后续按 sid 哈希落别处 → 404 → L3 resume 读不到 → 分叉;前缀路由把「id 决定副本」反过来变成「副本决定 id」。 副本序号即分片码:扩缩容不改既有映射;缩容副本上的会话随之丢失(与任何本地态服务同律,先 drain)。 自定义AgentSessionFactory须采用init.sessionId,忽略即失去粘性并收session.id_hint_ignored告警。副本故障 = 其上会话不可迁移(客户端按 404 → 新建)。恒不共享存储目录(NFS/EFS/同一卷挂 两副本):kernel 会话锁按本机 PID 判活,容器 PID 命名空间下跨副本必然误判——SESSION_LOCKED30 min 或并发写同一 journal →500 store_corrupted;这是不安全配置而非「慢一点」。 - 优雅关闭时序(宽限 45 s):t0 SIGTERM(+ K8s 摘 Endpoints)→ 拒新(
/readyz503、新建/resume503 draining+Retry-After、SSE 下发长retry:)→ 等在飞轮 ≤TANSR_SERVE_DRAIN_TIMEOUT_MS(缺省 30 s,到点interrupt(),轮末 store 提交照走)→ flush 落盘 → 停监听 ≤ 5 s + 连接收口 ≤ 5 s → 宿主governor.close()→ exit 0;t0+45 s 未退出 → SIGKILL。宽限恒 ≥ drain 超时 + 15 s。 - 观测端点暴露:容器绑
0.0.0.0时须TANSR_SERVE_EXPOSE_OBSERVABILITY=1(参考镜像已设),探针 与抓取器走内网直连副本;反代恒不把/metrics/healthz/readyz转到公网。十王修案 FX-C-42(O-5):TANSR_SERVE_EXPOSE_METRICS单独控/metrics(优先于总开关);CLItansr serve入口把/healthz/readyz钉为恒开(探针面无机密,父进程 tansrd 池 / K8s 探活不受EXPOSE_OBSERVABILITY=0影响),库形态仍按exposeHealth/exposeMetrics/expose三选项自定。 - 进程间就绪帧(FX-C-42;S2-04 / 候拍 ㉑):listening 后引擎恒额外写一行
TANSR_READY {"v":1,"url":"http://127.0.0.1:1234","pid":4242,"readyz":"/readyz"}到 stdout——它不是日志(不经ServeLogger、不受TANSR_SERVE_LOG_FORMAT影响、无 ts / level),是父子进程 协议(tansrd 池readyFrom第 ① 形;②server.listeningJSON 行 / ③ 文案行为兜底,旧 serve 兼容一版)。 日志采集器按行严格 JSON 解析时TANSR_SERVE_READY_STREAM=stderr改道,嵌入式宿主readyFrame:'none'或 传写函数自定;formatReadyFrame/parseReadyFrame是帧的唯一形态源(两端同笔)。 - 容量估算(本机现状档不外推;doc/118 §一-1.1):单进程 ≈2500 活跃 SSE 连接时 ELD p99 ≈190 ms、
5000 连接 ≈250 ms;≈35 KiB 堆/会话(全装配链);企业档参考帽
maxActiveSessions 5000/maxSseConnections 10000/maxInflightBodyBytes 256 MiB/ ELD p99 200 ms(G5,仅文档)。 fetchImpl须尊重init.signal:宿主自带 fetch(代理/埋点)包在 governor 外层或内层都可,但取消 传播链在此不得断——drain 到点的interrupt()、每轮墙钟、分层超时都靠 AbortSignal 抵达上游。
扩展档位(横向扩展的三级;doc/118 §八 G2 / §11.2)
| 档 | 形态 | 状态 | 重放窗(Last-Event-ID)在哪 |
|---|---|---|---|
| (a) | 单进程 + 分片粘性:v2.sessionIdShard + 反代前缀路由,副本私有存储根,恒不共享目录 | 已落(SC-32b/c;上节) | 创建副本内存(MemorySessionEventLog = 环形缓冲,条数 1024 + 可选字节帽) |
| (b) | 事件日志外置:AgentSessionsOptions.eventLog 注入 SessionEventLog 实现(Redis Streams / NATS JetStream 等) | 接口缝已落(SC-35);实现另包候需求,包名属命名面留用户 | 外部日志;任一副本可按 sessionId 重放,会话运行态(驱动/桥/在飞轮)仍在创建副本——分片路由仍需要 |
| (c) | 会话态外置:驱动/桥/历史随会话迁移到任一副本 | 未立项 | 外部 |
(b) 档的缝:eventLog: (ctx: { sessionId, endUserId }) => SessionEventLog,每次纳管一枚;引擎的泵写入
(append(event, frameBytes?) 返回事件自带的 seq)、Last-Event-ID 重放与 gap 帧判定(readAfter(afterSeq)
一趟返回 { events, oldestRetainedSeq?, dropped, gap })、ringStats 汇总(stats())、记录离开内存
(close(),保留窗到点 / 数量帽 / resume 让位 / closeAll 各恰一次)只经此接口。缺席 = 内存实现,行为
逐字节同今(eventBufferSize / eventBufferMaxBytes 仍施于缺省实现;传了工厂即由实现定容量与逐出)。
实现纪律:全部方法同步(泵与订阅的同步原子块依赖此契约,异步后端须在接口之下自做写缓冲/本地镜像);seq
由句柄分配、随事件携带,日志恒不改写(外部存储的自生成 id 只能作游标);引擎恒不引 zod 之外第三方,任何
外置实现都在别的包里。
分层存储(冷层):会话历史落到 S3 / OSS / KV / 关系库(doc/119 IO-21 / doc/120;IO-32 集成后可用)
会话历史的耐久性地板恒是本地热层(轮末 journal 追加,fsync + rename ≈ 13 ms/轮);热层不可能配无限硬盘,
所以既有 store 工厂加了可选冷层:热层达到封段阈值(缺省 16 MiB / 5000 记录)后把这一截封成不可变的段对象
(JSONL + gzip,首尾哈希链)上传到你挂的存储,再以 CAS 重写一份清单(唯一提交点);已提交段可被热盘 LRU 逐出,
读侧按需回源。不挂冷层 = 今日行为,字节等价;客户端零感知(/v2 wire 零改动)。
五行接入(createAgentSessionFactory({ store }) 不改;doc/91 冻结接口不改):
import { createServeAgentSessionStore, createAgentSessionFactory } from '@tansr/serve';
import { createS3BlobStore } from './store-s3.js'; // 你的适配器(参考实现 examples/store-s3,零依赖 fetch + SigV4)
const store = createServeAgentSessionStore({
dir: '/var/lib/tansr/hot', // 热层:本地盘,恒不共享
cold: createS3BlobStore({ bucket, region, credentials }), // 一级 SegmentBlobStore(或二级 SessionHistoryStore)
policy: { tenant: 'acme', hotRetention: { maxBytes: 20 * 2 ** 30 } },
logger, // 冷层事件 → 结构化日志 store.*
});
const build = createAgentSessionFactory({ cwd, store, platform: { ... } });两级选择:一级 SegmentBlobStore(5 个字节方法 put / get / head / list / delete + 一次能力自述
capabilities;S3 / OSS / COS / MinIO / R2 / GCS / Azure / GridFS / 文件系统皆此级,引擎内置分段、编码、清单 CAS、
热盘 LRU、重试、指标)——推荐;二级 SessionHistoryStore(store / load / listSessions / remove,自管布局;
KV / 关系库更贴合)。适配器唯一要遵守的错误约定是 StoreError.kind 四类(transient / permanent /
not_found / precondition_failed;其他异常一律按 transient)。一级冷层 conditionalPut:false 或 list:'none'
时必须再挂 index: SessionIndexStore(多副本共享会话元 / CAS 落点),否则构造期 fail-fast(不静默降级为不安全提交)。
随包实现:createMemoryBlobStore()(测试替身)、createFsBlobStore({ dir })(目录模拟对象存储;只可作冷层)。
一致性测试套 runStorageConformance(store) 认证你的适配器(fs / memory / S3 同一套)。
旋钮(policy):
| 键 | 缺省 | 语义 |
|---|---|---|
| codec | 'jsonl+gzip' | 段编码;'jsonl+zstd' 仅 Node ≥ 22.15 可用 |
| hotRetention.maxBytes | 不设 | 热盘上本地段文件占用帽:已提交段先逐出;仍越帽 → readiness() 红 + store.hot_full 事件;未上传段恒不删 |
| hotRetention.keepRecentSegments | 引擎缺省 | 逐出时保留的尾段数(压缩后 resume 常只需摘要 + 尾段) |
| upload.mode / concurrency | 'async' / 4 | 热层先 ack 再异步上传;并发受约束(A12) |
| upload.retryBudget | 8 次 / 退避封顶 60 s | transient 重试预算;耗尽 → store.commit_failed{gaveUp:true} + onStoreError |
| restore.prefetchTailSegments | 1 | 热层全失重建后预取的尾段数 |
| transform | 无 | encode / decode 字节流挂点:加密 / 脱敏归你(引擎不做密码学) |
| keyMapper | 恒等 | 规范键 <tenant>/<endUserKey>/<sessionId>/<volumeId>/… → 你的键(不得破坏唯一性) |
| tenant | 'default' | 规范键首段(多租户隔离) |
metrics 缺省 kernel defaultMetrics(与 /metrics 同源);index 见上;onStoreEvent(event) 原样订阅冷层事件;
onStoreError(sessionId, error) 只在放弃时回调(热层已 ack,恒不抛给调用方)。
失败语义:上传 / 清单提交失败恒不回滚热层(轮末已 ack 的历史耐久不变)——按预算重试,超预算记
store.commit_failed + 积压指标,段留在热盘、下次绑定时对账补传;precondition_failed(清单被另一副本接管)本写者
放弃不重试(A2 / A13)。读侧:回源段篡改 / 截断 / 跳段(permanent)→ 500 store_corrupted,恒不静默续接;冷层
暂不可达(transient)原样上抛 StoreError(不冒充损坏)。热层全失(节点重建,A10):同一冷层重开 store,
GET /v2/sessions 列表见会话、resume 经冷清单 → 段 → 重灌成功(只有已封存的段可恢复;未越阈值的活段随热层丢失——
需要更细粒度的耐久点请调小 segmentation 阈值)。delete 先删冷层再删热层。
两条告警线(/metrics,tansr_kernel_store_* 族:ops_total{op,result} / bytes_total{op} / latency_ms{op} /
failures_total{kind} / backlog_bytes / hot_bytes):
① tansr_kernel_store_backlog_bytes 持续增长 = 冷层不可达或写失败(热层仍在服务,但保留窗只剩本地盘);
② tansr_kernel_store_hot_bytes 逼近 hotRetention.maxBytes = 即将触顶——触顶后 /readyz 503(理由
store_hot_full,v2.store.readiness 经工厂 storeReader 转发即自动接线;或把 store.readiness 直接挂
observability.readinessProbes),LB 摘流、新建改落别的副本。结构化日志:store.segment_sealed /
store.segment_committed / store.commit_failed{kind,attempts,gaveUp} / store.hot_full / store.hot_recovered /
store.evicted / store.orphan_gc(字段恒不含消息内容)。
部署建议:热层恒本地盘、恒不共享(共享盘是不安全配置,见上节);冷层用生命周期规则承接保留窗
(<tenant>/<endUserKey>/<sessionId>/<volumeId>/ 前缀按 expiresAt / 标签过期;压缩换卷 = 新 volumeId,旧卷独立
过期不影响新卷);冷层里的段是明文 JSONL(压缩)——加密、密钥管理、驻留地、访问控制归你(transform 挂点),
endUserKey 是哈希不是身份,不要把 endUserId 明文放进键或标签;多副本下配 index(会话元共享、list 便宜),
policy.tenant 按租户隔离前缀。优雅关闭:drain 第三阶段在工厂 flush 之后再 await store.flush({ timeoutMs })
等冷层队列排空(返回 false = 超时仍有积压,热层已耐久,重启后对账补传)。
三观测端点(免 Bearer;缺省只在回环监听时暴露)
| 端点 | 语义 | 响应 |
|---|---|---|
| GET /healthz | liveness:进程活着即 200(关闭期亦 200,摘流看 readyz) | {"status":"ok"} |
| GET /readyz | readiness:!closing ∧ 全部就绪探针 ready | 200 {"ready":true,"reasons":[]} / 503 {"ready":false,"reasons":["closing",…]} |
| GET /metrics | OpenMetrics 1.0 文本(application/openmetrics-text; version=1.0.0) | 内核 + serve 指标一份 exposition,末行 # EOF |
暴露纪律:三端点免 Bearer,所以暴露面必须显式——host 为回环(127.x / localhost /
::1)时缺省暴露;绑 0.0.0.0 / 公网地址时缺省不暴露,须 observability: { expose: true }
(或分别 exposeHealth / exposeMetrics)。未暴露时这三条路径落回既有流程,与任意
未知路径响应完全一致(无 Bearer 401 / 有 Bearer 404),不泄漏存在性。容器/K8s 部署把
它们只发布给探针与抓取器所在网络(反代前置时不要把 /metrics 转到公网)。
const server = await startServer({
// ...
observability: {
expose: true, // 绑 0.0.0.0 时显式放开
readinessProbes: [() => (storeWritable ? true : { ready: false, reason: 'store_unwritable' })],
requestLogSampleRate: 0.1, // 高并发下调请求日志采样;错误级事件恒不采样
},
});
server.stats(); // 结构化快照:sessions{active,byFace,pending?,retainedEnded?} / sse / http / eventLoopDelayMs / memory / notifier? / readiness指标(首批;名恒 tansr_ 前缀,process_resident_memory_bytes 为唯一标准名例外)
内核层(五形态共享,TUI/Headless/serve/acp/SDK 同一份进程级读数;kernel 出口
defaultMetrics / createMetricsRegistry / renderOpenMetrics 经本包再出口):
tansr_kernel_model_calls_total{result}、tansr_kernel_model_call_duration_ms、
tansr_kernel_tool_executions_total{result}、tansr_kernel_tool_duration_ms、
tansr_kernel_turns_total{reason}、tansr_kernel_internal_errors_total、
tansr_kernel_compactions_total{mechanism}、tansr_kernel_retries_total{class}。
serve 层:tansr_serve_http_requests_total{method,route,status}(route 恒模板化,如
/v2/sessions/:id/events;status="0" = 响应头未发出即断开)、
tansr_serve_http_request_duration_ms{route}、tansr_serve_rejections_total{code}
(401/403/404/405/409/410/413/422/429/4xx/5xx)、tansr_serve_sessions_active{face=v1|v2}、
tansr_serve_sessions_pending、tansr_serve_sessions_retained_ended、
tansr_serve_sse_connections_active、tansr_serve_sse_frames_written_total、
tansr_serve_replay_gap_total、tansr_serve_ring_evictions_total、
tansr_serve_event_loop_delay_ms{stat=p50|p99|max}(两次采集之间的窗口)、
process_resident_memory_bytes、tansr_serve_webhook_deliveries_total{result}、
tansr_serve_store_commit_errors_total / tansr_serve_store_create_errors_total、
tansr_serve_token_mint_failures_total、tansr_serve_token_invalidations_total{reason}(FX-C-15:上游 401
裁定令牌失效,按信封 detail.reason 分键;每次伴随失效 + 重铸 + 重试恰一次)。
纪律:指标标签恒不含 sessionId / endUserId / 令牌 / 消息内容(高基数与凭据禁令);
未知路径一律 route="other"。/v2 的 route 模板与路由分发同出一表 V2_ROUTE_TABLE(导出;21 端点 ×
{ method, action(模板), pattern, endpoint },FX-C-45)——此前观测层手写 7 动作,compact / checkpoints 全族 /
cwd / audio 九条路径落 other;今 other 只对域内 404 出现(锁卷 observability-route-coverage.test.ts)。
同进程起多台 startServer 且共享缺省注册表时,gauge 族由最后
采集者覆写、counter 族累加——需按实例隔离读数时各传 observability.metrics: createMetricsRegistry()。
结构化日志与 request-id
ServeLogger 新增可选 log(level, event, fields)(旧 info/error 保留)。事件名恒英文
机器码(LOG_EVENT 词表:request.completed / request.unhandled / sse.opened /
sse.closed{reason} / store.commit_failed / store.create_failed / notify.delivery_failed /
server.listening / server.closed …;分层存储冷层在场时另有 store.segment_sealed /
store.segment_committed / store.hot_full / store.hot_recovered / store.evicted / store.orphan_gc,
见上节),字段恒不含令牌与消息内容(sessionId 可在场,
endUserId 仅 /v2 已鉴权时由路由层回填)。只有旧形 info/error 的 logger 会收到回落:
带人读文案的事件(listening/closed/兜底错误)仍是既有 i18n 文案行,其余事件为一行 JSON
{"level","event",...fields}。
request-id:每个请求采纳合法入站 x-request-id(^[A-Za-z0-9._-]{1,128}$),否则取
W3C traceparent 的 trace-id,再否则自铸 UUID;恒回响应头 x-request-id(含 401/404),
并作 request.completed{requestId, method, route, status, durationMs} 与
request.unhandled 的关联键。SSE 长连接在连接关闭时发一次 request.completed(sse:true)。
createAgentSessionFactory 的 onStoreError 缺省不再静默:一行结构化 JSON 到 stderr 并计数。
容量基线与门禁
bench/serve/(A/B/C 路审计脚本)已门禁化:pnpm bench:serve:gate 按 bench/serve/thresholds.json
现状档断言(本机数字为参考、不外推;目标档随 G12 回填),详见 bench/serve/README.md。
容量模型:「挂着的会话数」≠「同时活跃数」——订阅者全部离场 orphanGraceMs(60 s)后中止当前轮、再
idleAfterGoneMs(5 min)后落盘退出内存,resume 复活;内存与 CPU 只随同时活跃的会话增长。单进程天花板是
单事件循环 CPU(几千会话同时流式),准入帽(admission)让它有界拒绝而非拖垮,v2.sessionIdShard 分片前缀
路由随时加副本。部署前置:每条 SSE 占 1 个文件句柄,Linux 缺省 nofile 1024 会把并发订阅者卡在一千出头
并报 EMFILE——按目标并发 ≥ 2× 抬(deploy/serve-v2 模板已置 65536,nginx 侧 worker_connections 同律)。
上游治理(平台内置形:铸令牌闸 / bundle 缓存 / 503 语义 / governor 注入)
平台内置形(createAgentSessionFactory({ platform }))的上游链是
appid/appkey → POST /v1/app-tokens(per-endUser 令牌)→ GET /t1/config + POST /t1/heartbeat
(bundle/features)→ POST /t1/exchange(模型)。Wave 2(SC-22/23/24/30)起这条链有了治理位;
缺省值全部为建议值(候 G3 拍板),upstreamStats() 可读:
const build = createAgentSessionFactory({
cwd,
logger, // 缺省提示帧承接经 platform.warning 结构化事件(缺席回落 console.warn)
platform: {
apiBaseUrl, appId, appKey,
tokenMinter: { // 铸令牌治理(SC-23/SC-30;全部可选)
maxConcurrentMints: 32, // 全局并发闸:同时出网的 /v1/app-tokens 数(重启惊群从 N 降到闸值)
maxQueuedMints: 1024, // 闸等待队列;满额即刻 503 overloaded(不出网)
mintQueueTimeoutMs: 10_000, // 排队超时 → 503 overloaded
expiryJitterRatio: 0.15, // 令牌 expiresAt 只提前不推后的抖动比(打散 TTL−300 s 的对齐刷新)
negativeCacheInitialMs: 5_000, negativeCacheMaxMs: 60_000, // 失败负缓存 5 s 起指数 ×2 至 60 s
maxCachedTokens: 100_000, // 令牌缓存 LRU 帽(过期项读到即删)
},
bundleCacheTtlMs: 60_000, // bundle/features 缓存 TTL 上界(SC-24;0 = 每会话拉取)。也可传 bundleCache 实例跨工厂共享
governor, // SC-22:providers createUpstreamGovernor 产物;三处出站同一枚(见下)
},
});
build.upstreamStats?.(); // { tokenMinter: { inflight, queued, mints, hits, negativeHits, staleServed, failures{kind}, cached, evicted }, bundleCache?: { hits, misses, revalidations, notModified, inflightDedup, … } }- 铸令牌闸 / 抖动 / 负缓存:500 endUser 同时首连时
/v1/app-tokens在飞峰值 = 闸值(此前 500); 同批铸出的令牌不再同刻进入刷新窗;某 endUser 铸造失败后窗内不再出网(直接回同一错), 刷新失败但旧令牌仍在时效内直接沿用旧令牌;warm(endUserIds)可在启动期预热。 - 令牌失效反馈(十王修案 FX-C-15 / R-02):平台对携
x-tansr-app-token的请求(模型 exchange / bundle / 系统工具的平台提供方)回401 { error: { code: 'unauthorized', detail: { reason } } }时,刷新 fetch 在同一请求内minter.invalidate(仅当缓存现值仍是本次所携令牌)→ 重铸 → 重发恰一次;重试仍 401 则原样交回(每请求至多 一次重铸,恒不循环)。reason === 'check_unavailable'(平台校验依赖瞬态不可用)不失效、不重铸;其余码 / 403 / 无信封亦不触发。此前吊销令牌后 serve 拿旧令牌 ≈ 55 min 恒 401。计数:stats().invalidations{reason}(只计真正逐出)+ 指标tansr_serve_token_invalidations_total{reason}(按 401 裁定计;自实现 minter 缺invalidate时只计不重试)。失效码集合TOKEN_INVALIDATING_CODES = ['unauthorized'](导出;M-13 起改由 openapix-invalidates-token生成到 sdk,serve import)。 - bundle 缓存:键
(apiBaseUrl, appId, scope, featuresFingerprint)(FX-C-16:径别app_user|owner+ 客户端 features 声明指纹;serve 恒令牌径,宿主自有 owner 径消费方共享同一实例亦不串档),TTL = min(平台max-age,bundleCacheTtlMs);过期后If-None-Match条件重拉(304 只续期);features(heartbeat)按 endUser 每 TTL 一次而非每会话。 建会话上游往返:冷 endUser 3 → 1(仅铸令牌;首次 +1 config),热 endUser 2 → 0。 一致性 SLA:控制台改模型授权/别名后在网关生效的延迟上界 =bundleCacheTtlMs(缺省 60 s); 要更快生效就调小(平台max-age仍是下界之一),要即时一致传0。 - 503
upstream_unavailable(候 doc/98 §七 增笔 G4-a):平台铸令牌撞 429 / 5xx / 网络错时建会话 回503 upstream_unavailable并携Retry-After(取平台Retry-After,缺席按admission.retryAfterMs5 s);客户端按 5xx 语义退避重试即可。与503 overloaded(本机闸忙 / 准入帽)分开:一个是「上游坏, 加压无益」,一个是「本机忙,稍后再来」。4xx(凭据/入参被拒)维持400 create_failed(重试无益)。 指标tansr_serve_token_mint_failures_total计每次上游铸造失败(负缓存命中不重复计)。 - governor 注入(SC-22 serve 半场):治理核(显式
undici.Agent连接上限 / per-origin 隔离舱 / 熔断 / 重试预算 / 本地令牌桶)落@tansr/providerscreateUpstreamGovernor(options);serve 只负责把 同一枚进程级实例的fetch注入三处出站——platform.governor(铸令牌器 + bundle 缓存 + 模型 client)与startServer({ governor })(轮末 webhook 通知机)。推荐:一进程一枚,connections按平台 连接配额、keepAliveTimeout ≥ 60 s;宿主持有其生命周期(governor.close()在server.drain()之后 由宿主调用,serve 恒不代关);宿主自实现fetch时必须尊重init.signal(取消传播链在此不得断)。 未传 governor = 直连fetch(行为零漂移)。 - G3 平台限流上报:平台现行限流键绑 app key(
/t1/exchange120/min)+ 源 IP(600/min);网关形态 下全部 endUser 共用一把 key、一个出口 IP,本节的闸/缓存只能削峰、不能抬顶——upstream_unavailable持续出现且Retry-After对齐到分钟整点,即触顶信号,应上报平台侧调档(总卷 G3 / C 路 D-1)。
应用系统提示词与移动端(AP-SP)
平台内置形的 platform.system 为宿主业务段;未设置时使用平台应用 systemPrompt。默认 fallback 下显式值(含 [])替换平台段;平台 systemPromptPolicy=prepend 时保留平台段在前,再拼宿主段,[] 也不会移除平台段。platform.systemAppend 追加宿主工具使用指南,不触发覆盖。P 与 S 是同一 system 层输入,拼接不是权限或冲突裁决机制。
Android/iOS 经 /v2 连接本服务,用户 prompt 不是 system 字段;配置放在平台或 Node 宿主,客户端不传 appkey。平台保存正文或策略后,已有会话在下一轮开始前按 ETag 验证配置,绕过 60 秒装配缓存;会话历史继续保留,正在执行的一轮(含工具多步)使用该轮提示词快照。宿主 S/A 仍在建会时固定;恢复历史不阻止下一轮刷新。刷新失败会以 application_prompt_refresh_failed 和 turn.aborted 中止本轮,不带旧配置发起模型调用;刷新默认最多等待 30 秒,可取消后重试。工具、权限、模型和计价等其他配置仍沿原装配生命周期。示例及完整矩阵见 serve-demo 与 提示词指南。此变更尚需正式发布包含 AP-SP 的版本。
MEDIA-03 历史媒体恢复
媒体产物随工具回执保存,恢复后的 result 可复用现有媒体组件;artifactUnavailable 单独表示预览不可恢复,不代表工具失败。首次写产物会启用存储版本 2,请统一升级写者并先备份。配置、TS 示例及英文说明见媒体历史恢复 / Media history recovery。本能力已随 @tansr/[email protected] 发布,须同时升级消费端。
