@cubos/代理服务器
库博斯特工的客户端 两个表面: 对话,用于您的最终用户应用程序 管理员为了你的后端.
零从属. 仅使用 fetch, (中文(简体) ), ReadableStream 财务报告和审定财务报表 AbortController,所以同样的代码运行在浏览器,节点 \ 18, Bun, Deno和Cloudflare工人.
bun add @cubos/agent-sdk # or npm / pnpm / yarn
代号: 由您的后端铸造, 绝不在客户端中
一个Cubos Agent api key是全租户,而且 绝对不能到达浏览器您的后端将它切换成一个短命的符号, 范围可限制给一个用户 :
import { createAdminClient } from "@cubos/agent-sdk";
// On YOUR server.
const admin = createAdminClient({ baseUrl, apiKey: process.env.CUBOS_API_KEY! });
const { token, expires_at } = await admin.tenant("acme").userTokens.create({
user_id: session.userId, // from YOUR auth, never from the client
agent_slugs: ["support"],
ttl_seconds: 900, // optional; defaults to 3600
});
密钥需要 : user_tokens:write 赠款。 由此而来的信使只到达了对话的终点,只有该用户自己的对话——无法从客户端扩大它.
agent_slugs 列出该符号可能打开对话的代理. ttl_seconds 默认为3600,上限为86400. 没有撤销列表:更喜欢最短的TTL你的应用可以舒适地刷新. 封锁用户( E)users.block)在下次请求时,其令牌无效.
对话
import { createUserClient } from "@cubos/agent-sdk";
const client = createUserClient({
baseUrl: "https://agent.acme.com",
// Called per request — cache it yourself. On forceRefresh, return a freshly
// minted token (the SDK only asks for that after a 401).
getToken: async ({ forceRefresh }) => fetchTokenFromMyBackend({ forceRefresh }),
});
const conversation = await client.createConversation({ agentSlug: "support" });
await client.sendMessage(conversation.id, "My invoice was charged twice");
const subscription = client.subscribe(conversation.id, {
onMessage: (m) => console.log(m.role, m.content),
onActivity: (a) => setTyping(a.isProcessing || a.hasPendingTurn),
onTodos: (todos) => setPlan(todos),
});
subscription.close();
如果你已经持有一个符号——一个脚本,一个测试——直接通过作为 token: "…" 而不是写一个结束。 静态符号不能被更新, 因此它最终会永远过期; 使用 getToken 对于任何真实的东西.
subscribe 回填连接上的历史,然后从任何下降后的确切点恢复,所以它提供的是整个对话——不需要将其与 listMessages,用于向后呼回漫长的历史.
告诉探员屏幕上的内容
await client.setContext(conversationId, `screen: customers\nfilters: status=active`);
只要屏幕动了就叫它——它不写事件,也不开始转弯,所以它 在每次航行,甚至是中转时,都是安全的。 服务器向代理显示 下次一转看对话, 只有当它改变,所以 重复一个不变的上下文并不花费什么.
这是原始路线. @cubos/agent-sdk-react是因为 useConversation({ context })
- 浏览11页
输入任何内容之前的屏幕会发出一个请求,而不是十一个.
应用程序所能达到的替代方案是将上下文前缀在用户的上 留言。 将它花在每一个消息, 把它放在记录,除非每一个 使路径剥出它, 并且只为谁拥有作曲家.
onActivity 在同一条溪流上,订单是您可以基于的保证:一个框架说结束的转弯在结束它的答复之前从未交付. 关闭上面的打字指示器, 答案已经掌握在手了 。 (它过去来自对话-元数据流,第二个连接,没有针对日志的订单,它们之间的间隙——一毫秒在本地插座上,几秒通过一个代理——显示一个完全的转折,里面什么都没有. )
对于一个错误交易的长篇对话:每一次重联重播整个日志. 通过一个光标, 溪流从那里开始, 留给历史 :
const page = await client.listMessagesPage(id, { limit: 50 });
setMessages(page.messages);
// Only what is newer than the page. Nothing is lost in between: the server
// catches up everything past the cursor before going live.
client.subscribe(id, { onMessage: append }, { since: page.latestChangeSeq ?? undefined });
// Scrolled to the top — the page before this one.
if (page.hasOlder) {
const older = await client.listMessagesPage(id, { before: page.oldestSeq, limit: 50 });
prepend(older.messages);
}
@cubos/agent-sdk-react 电线完全接通 loadOlder 编号 : hasOlder。 。 。 .
日志本身,当谈话不够时
对话表面故意隐藏事件日志: 一个聊天抽取消息,
没有 llm_call 作行相会. 但隐藏并不等于隐瞒
订阅同时携带,需要日志的应用程序从同一处获得
地点 :
const page = await client.listEventsPage(id, { limit: 50 });
client.subscribe(
id,
{
onEvent: (e) => append(e), // every row, in stream order
onMessage: (m) => draw(m), // …and what it became, if you want both
},
{ since: page.latestChangeSeq ?? undefined },
);
一个连接,一个光标,一个赶上. onEvent 预测前的火灾
看相框,所以一个应用 摄取两者都不必调和.
不同的是承诺,而不是进入. Message | Todo | Activity 实值
- 保管并留在内部; ConversationEvent 这是
服务器自有的 DTO , 当服务器运行时移动—— 相同的交易 REST API
已经提供,因为这些行是 它返回。 帮助它建立一个
操作控制台、 审计视图或活动树; 停留在 Message 用于建设
一个聊天.
@cubos/agent-sdk-react 把它接进 useConversationEvents。 。 。 .
重新打开是免费的
已经上载的谈话被保存在一个有界限的记忆中,所以打开一个 第二次没有要求的油漆:
const start = await client.loadHistory(id); // start.fromCache on a repeat
client.subscribe(
id,
{ onMessage: append, onCursor: (seq) => (cursor = seq) },
{ since: start.latestChangeSeq ?? undefined },
);
// Hand the current state back whenever it moves; the next open resumes from here.
await client.saveHistory(id, { messages, oldestSeq, latestChangeSeq: cursor, hasOlder });
击打不是呆子. 服务器的事件日志从未重写一行 content, (中文(简体) ),
seq 或者说 type,以及每个确实发生的突变——一个暂定事件是:
合并或丢弃的交货收据——通过触发器
发行新版本 change_seq。因此订阅缓存光标重放
在应用程序离开时 究竟有什么变化 插入和更新都一样.
过 cache: null 关掉它, cacheMessageLimit 以改变多少
每次谈话都会保留(默认情况下300个消息;旧信件从
缓存,不是服务器,然后通过呼号返回。 为了活过重载,
执行 ConversationCache 超过索引数据库、 AsyncStorage 或 SQLite :
interface ConversationCache {
read(key: string): Promise<CachedConversation | null>;
write(key: string, entry: CachedConversation): Promise<void>;
clear(key?: string): Promise<void>;
}
钥匙已经送到租户和用户的手中——一个持续储存的商店由 设备上的每个会话,两个用户不能互相读取对方的信件 离开它。 视之为不透明. 一个拒绝的商店被当成一个小姐,所以 腐败的缓存会降解为网络,而不是打破聊天.
租户通过象征性方式解决 GET /me 在第一个呼叫。 过 tenant 在跳过往返的选项中.
倾斜
当您想要所有的东西并且不愿管理光标时 :
for await (const conversation of client.iterateConversations()) { … }
// The 20 most recent messages, newest first.
const recent = [];
for await (const message of client.iterateMessages(id)) {
recent.push(message);
if (recent.length === 20) break;
}
突出不会获取下一页 .
pageSize 计数 事件,而不是消息:日志还持有代理工具的呼叫和簿记,因此一个页面可以持有比您要求的更少的信息. 执行者对此负责;一页没有消息,不会结束行走.
反应部分
您的应用程序可以让代理回答 组成部分——价格图,a 取出取枣者,取出定单牌来取取取——而不是单独打分. 代理人可能使用的东西 来自 组件库a. 管理API的操作作者; 您的应用程序允许打开屏幕可以绘制的 :
const conversation = await client.createConversation({
agentSlug: "support",
componentLibraries: ["support-widgets"],
});
setComponentLibraries(id, slugs) 随时替换列表 — 发送
完成 ; 空列表将代理人带回平面标记下。 变化
从代理的下一个回合.
分出即是信相界. 构成部分摘要、说明和 向模型显示示例 逐字记录,所以写作是快速的 内容和需要一个 api key; 启用库只是线程, 这就是为什么 这个客户端 持有一个短命的最终用户标志 能够做到这一点, 仅此而已.
授权呼叫及其 list 对应回复 tags,每个
包含启用的库决心,按照代理查看的顺序
已索引。 对照您可以实际渲染的内容检查: 没有渲染器的标记是
一个阻隔 代理将乐于写 和没有画.
每条代理信息都会被分割成 blocks,已解析道具
并对照图书馆的计划:
{
message.blocks?.map((block, i) =>
block.type === "markdown" ? (
<Markdown key={i}>{block.text}</Markdown>
) : (
<MyComponent key={i} tag={block.tag} {...block.props} />
),
);
}
一个组成部分是: 总是自我封闭 没有 <Tag>…</Tag>. 无论如何
应该用道具来显示 周围的标记会留在外面
自有块 :
// leia **isto**
// <PriceChart symbol="PETR4" points={[1,2]} />
[
{ "type": "markdown", "text": "leia **isto**" },
{ "type": "component", "tag": "PriceChart", "props": { "symbol": "PETR4", "points": [1, 2] } }
]
值得我们知道:
blocks总是在代理信息. 无组件的答复是 一个markdown块,所以你写一个渲染路径,而不是两个.- 服务器执行合同. 没有启用的图书馆服务组件,
HTML,或者在信件被写入之前,错过了"计划"的道具被拒绝——
代理人得到错误并重写。 验证是完整的JSON Schema,不是
浅色类型检查,拒绝时指定失败的路径
(单位:千美元)
points.0.v: "muito" is not of type "number"),所以代理可以修复. content仍然是整个消息 如探员所写,如果你愿意 不如自己来.- 块是读取时产生的,从不存储. 丢弃库不会 重写旧消息,所以保留一个渲染器 任何记录可能仍然 包含.
- 每次对话都启用库, 只有当您的应用程序是渲染器时才启用 . 在频道支持的对话(WhatsApp, Telegram)上进行 外部频道则在频道上声明其库.
- 交互是您的: 代理输出块, 您的组件处理
点击,不管它意味着什么
sendMessage。 。 。 .
现场对话列表
const page = await client.listConversations();
client.subscribeToConversations({ onConversation: (c) => upsertAndResort(c) });
每当对话活动取得进展时,就再次发出,这就是保持 一张没有投票的聊天列表.
图像
await client.sendImage(id, file, { caption: "is this the right charge?" });
// Up to 10 in one message, so the agent reasons over the set rather than one
// turn per picture. A label names an image for the model, which lets it answer
// about "the receipt" instead of "the second image".
await client.sendImages(
id,
[
{ image: receipt, label: "receipt" },
{ image: statement, label: "statement" },
],
{ caption: "compare these" },
);
png,jpeg,webp和gif,每个可达10MB. 特工在它的模型有视觉时就从原生地看待画面,否则就是从特工倒置的视觉模型中描述出来——无论是哪种方式,都花费一转并作为一个预算.
一个图像像一个 Message 谁 attachments 说明媒体及其 content 是标题. 没有标题的发送是正常的,所以渲染 attachments 即使当 content 为空. 字节又回来了 Blob,而不是URL,因为信使在信笺中行走——a <img src> 指向航道将到达无证:
const blob = await client.fetchAttachment(conversationId, message.id, attachment.id);
img.src = URL.createObjectURL(blob); // revoke it when the element goes away
当反向视觉模型描述一幅图片时,该描述不包含在 是模特在自言自语 屏幕,而不是用户发送的信息.
语音信息
await client.sendAudio(conversation.id, blobFromMediaRecorder);
片段由代理商的STT模型在转弯运行前进行转录. 它像一个 Message 携带一个 audio 附件,与 content 持有抄本——直到STT完成为空,这是客户端显示玩家尚未下方文本的窗口. 回放的字节来自 fetchAttachment 我也一样.
客户工具
没有服务器侧执行的工具:代理机呼叫一个, 轮到它了 暂停,你的应用程序答案.
const session = await client.serveClientTools(id, {
tools: {
get_location: {
description: "Where the user is, as their device reports it.",
inputSchema: { type: "object", properties: {} },
readOnlyHint: true,
handler: async () => ({ city: "Fortaleza" }),
},
},
});
宣布节目,观看谈话 打电话,租赁每一个这样两个 选项卡不运行两次,在慢处理者工作时延长租约,并重试 效果 POST — 一旦副作用出现, 答案就值得多次尝试 已经发生了.
它打开自己的活动流来进行监视. 如果您已经订阅 谈话,不要支付第二个连接:
const session = await client.serveClientTools(id, { tools, watch: false });
client.subscribe(id, {
onOpen: () => session.poke(), // catches up after a (re)connect
onClientToolCall: () => session.poke(), // and on every new call
onMessage: append,
});
setClientTools 宣告时不运行任何内容——在
第一个信息,因为一个工具 从来没有告诉过代理 无法调用在
转过来. @cubos/agent-sdk-react 做这一切为你.
对话表面
me() 编号 : refreshIdentity() | 信使的用户、租户和代理人 |
listConversations() 编号 : iterateConversations() | 带有光标的一页或全部 |
createConversation() | 打开与代理人的对话 |
getConversation() 编号 : renameConversation() 编号 : archiveConversation() | |
listMessages() 编号 : iterateMessages() | 历史 |
listMessagesPage() | 历史加光标以呼回和指针 subscribe({ since }) |
loadHistory() 编号 : saveHistory() 编号 : forgetHistory() | 对话缓存 |
sendMessage() 编号 : sendAudio() | 用户消息,输入或已发 |
sendImage() 编号 : sendImages() | 一个图像,或者一个信息中最多10个 |
setClientTools() 编号 : serveClientTools() | 声明您的功能,并运行调用 |
setComponentLibraries() 编号 : listComponentLibraries() | 代理服务器可能使用的组件库 |
fetchAttachment() | 作为附件的字节 Blob |
subscribe() 编号 : subscribeToConversations() | 状态 |
他们都接受 AbortSignal。 。 。 .
暴露类型Conversation, (中文(简体) ), Message, (中文(简体) ), Attachment, (中文(简体) ), Block, (中文(简体) ),
EnabledComponents, (中文(简体) ), Todo, (中文(简体) ), Activity)是一个被管理
表面, 窄于服务器的 DTO : 事件日志有数十种类型
给操作员仪表板 将这里的仪表板钉上
内部重塑一个突破性的变化.
管理员
以 api key 认证的全 API, 按资源和范围命名 每个租户:
import { createAdminClient } from "@cubos/agent-sdk";
const admin = createAdminClient({ baseUrl: "https://agent.acme.com", apiKey });
await admin.tenant("acme").agents.list();
await admin.tenant("acme").users.block(userId);
await admin.tenants.list();
await admin.apiKeys.rotate(keyId);
类型来自服务器的 OpenAPI 文档(Schemas["Agent"],(......),
在每个路由更改中重新生成——所以客户端自行跟踪服务器.
admin.raw 是尚未被包裹的路径的逃生舱口.
仅限服务器侧. 一个 api key 是全租户的: 它能读写
每一个用户在租户的对话。 使用 createUserClient 在浏览器中.
错误
SDK抛出的一切 AgentError,所以一个 catch 涵盖两个
重要案例:
try {
await admin.tenant("acme").agents.get("support");
} catch (err) {
if (err instanceof AgentNetworkError) {
// Never reached the server: wrong URL, server down, CORS, or the timeout
// (`err.timedOut`). `err.cause` holds the original failure.
} else if (err instanceof AgentApiError) {
// The server answered, and refused.
if (err.isNotFound) …
if (err.isConflict) … // 409
if (err.isAuthError) … // 401/403
if (err.isRetryable) … // 429 or 5xx
}
}
AgentApiError 携带 status, (中文(简体) ), requestId 和逐字记录 body
(单位:千美元)err.json() 来分析它. AgentConfigError 由客户端构建而来
而不是打个电话——通常是 baseUrl 没有 http://。 。 。 .
每个请求都有30s的超时;用 timeoutMs,或 0 以禁用.
溪流是豁免的,因为保持开放是重点.
费率限额
服务器预算每个实例的最终用户令牌:大约20个代理转出,300个
其他电话每分钟, 加上一个同时流的上限。 拒绝是429
Retry-After,以及 sDK处理它 它等待广告的间隔
默认情况下重放请求两次. 429表示拒绝,而不是半申请,所以
重放一个 POST 这是安全的.
它放弃了,当复试用完,没有 Retry-After
或等待超过20秒时——阻断呼叫者
几分钟比现在告诉他们还糟. maxRetries: 0 选择退出。 中止期间
等待会取消重试而不是完成它.
api keys 豁免这一切.
瞬间流失败不结束订阅: SDK 重新连接
指数反转并报告通过 onError,用于伐木或a
"重联"提示.
三个看起来像是沉默的失败得到处理而不是报告。 A 类
401 (中文(简体) ) 购买一个立即重新连接的硬币刷新——短寿命
在流出超过预期的流水中过期的,不是
错误的配置——只有第二次拒绝才是最终的. A 类 429个 ——每个用户
在打开的溪流上盖, 一个标签太多到达了—— 等待服务器的
Retry-After 并重新连接, 因为盖子清除了任何标签关闭的时刻。 A 类
停止发送的连接 任何东西中保留保留备注
放弃并重新开放 idleTimeoutMs (30s,是服务器的两倍)
间隔 : 否则半开插座让读者永远等待
没有反应错误 .
一条溪流必须 在它被信任之前证明自己。服务器发送一个
每个连接上都有一个框架,所以直到一个连接发出一个——并且再次
只要一结束.. subscribe 投票记录相同 每3次通过 after
光标, 和对话排行。 在一个健康的网络上,框架土地
第一次民意测验的2分之多 从未有过民意测验 在一个网络上
代理持有一个 text/event-stream 回复直到结束( 公司 TLS)
检查,一些抗病毒药物——克罗姆与说话的宿主谈判HTTP/1.1
h2是事实,每一个连接 静静地静静,直到有东西切断它
答复以前只在重新装入一页后才出现;现在它在3s之内出现. 这个
相同的规则涵盖了一个下注和后面的再连接之间的间隔. 一个
光标为两条路径服务, 因此没有发送两次 .
最终结果是什么? 4xx幸存下来, 被删除的对话..
关闭对善和火灾的订阅 onFatal (单位:千美元)onError 见之也.
这是一个UI必须浮出水面: 一切都会变成,没有东西会
永远到来.
还在包裹里
@cubos/agent-sdk/sse——Server-Sent Eventions的阅读器本身(重接,光标重放,倒放),为消费者直接与API交谈,只想要那块.@cubos/agent-sdk-react——与回放钩相同的对话表面,在单独的包中. 胡克:它使无所事事,所以聊天UI保留你的.@cubos/agent-sdk-react-dom——少数通用性足以共享的组件,从代理人的平分开始. 只有Web,它本身也风格.