@empyria /重申
Restate.dev 助手的 Empyria,构建一个纳米服务框架 主要在Bun上:一个Admin API客户端,一个通用的动态调度服务,非驱动的 工作流调度,以及支持状态的输入/输出模式验证。
目标重状态服务器 1.7.x 和 @restatedev/restate-sdk 1.17.x.
要求
- 包子
>=1.4.0或node . js>=26 - 简单的ESM,没有构建步骤,没有TypeScript
这两个要求都来自 @empyria /分类
和 @empyria /常见这个包裹
取决于和哪个使用本机 Temporal 全局用于所有日期/时间处理。
安装
bun add @empyria/restate
使用
import { createRestateAdmin, withValidation, cronJob, cronJobInitiator } from '@empyria/restate'
const admin = createRestateAdmin({
restateAdminURL: 'http://localhost:9070',
restateURL: 'http://localhost:9071',
})
const services = await admin.listServices()
所有的东西都是从包根目录通过 index.js。个人
模块下 lib/ 如果只需要一个,也可以直接导入:
import { checkServiceHandler } from '@empyria/restate/lib/Admin.js'
模块
| 模块 | 目的 |
|---|---|
| lib / Admin.js | Restate管理API客户端——服务/处理程序发现、部署注册和拆除、工作流/消息调度; defineWorkflow/defineService/defineObject 构造定义器,和 setupRestate 用于连接具有健康检查和正常关闭的端点。 |
| lib / Caller.js | CallerServiceDef -一个通用的Restate服务,当目标在编译时不知道时,它通过名称调度对任何其他服务的任何处理程序的调用。 |
| lib / Cron.js | cronJobInitiator/cronJob -重播服务实现非驱动的、重播安全的重复作业调度 croner. |
| lib / Pubsub.js | definePubsub (在端点上注册一个公共订阅虚拟对象), pubsubPublisher (从处理程序内部发布到它,在进程中), pubsubClient (通过网络从其他任何地方发布/拉取/订阅)-薄包装 @restatedev/pubsub/@restatedev/pubsub-client. |
| lib / Validation.js | withValidation 封装一个带有输入/输出JSON模式验证的工作流处理程序 @empyria/common. |
| lib / Endpoint.js | serveEndpoint -在HTTP/2上提供服务/工作流,而不注册自身或在退出时删除其部署(对于注册地址的主管)。 |
代理人: @empyria/restate/agent
作为重构构建块的LLM代理(以前是 @empyria/restate-llm). 单独的条目
点,所以根导入永远不会加载AI SDK。它需要可选的对等依赖项:
bun add ai @ai-sdk/openai-compatible @ai-sdk/provider
| 模块 | 目的 |
|---|---|
| lib /代理/ Agent.js | defineAgent -构建服务/对象及其 ask handler:已验证的I/O; loadContext 它只记录技能索引、会话内存(对象状态或外部 historyStore)、步骤/令牌预算、调用重试策略。 createAgentService 是否已弃用? AgentService 快捷方式。 |
| lib /代理/ Loop.js | runAgentLoop -一次一轮;每个LLM调用和工具调用都是它自己的持久步骤。期刊模式的回应,从来没有提示或以前的历史。 |
| lib /代理/ Tools.js | 工具规格和执行:块工具(ctx.genericCall),执行工具(有界) ctx.run 使用幂等键),通过带有超时的可唤醒对象进行批准, load_skill, final_answer. |
| lib /代理/ Skills.js | 技能体不会出现在日志中: load_skill 记录 {name, sha256}, expandSkillRefs 将主体放回每个LLM步骤中。一个调用中的散列不匹配是终止的;跨会话回合使用当前主体。 |
| lib /代理/ Errors.js | toRestateLLMError (AI SDK错误→终端/ RetryableError / transient)以及LLM步骤、工具步骤和代理调用的默认重试策略。 |
| lib /代理/ Model.js | createModel - openai兼容端点(如Bifrost)的AI SDK模型。 |
重复工作
cronJob (虚拟对象)在cron调度上运行目标处理程序:每次运行发送目标,然后调度
从当前时间开始的下一个,因此重新启动或中断永远不会导致运行积压。注册 cronJob 在你的
端点,然后使用以下任意一种创建作业:
cronJobInitiator.create(request):每次调用一个带有随机ID的新作业(适用于用户创建的作业);ensureCronJob({restateURL, id, cronExpression, service, method, key?, payload?}): 幂等的,对于代码 从应用程序开始。 这份工作很稳定id你给它,所以每次开始时调用它不会增加一秒 作业:同样的请求不会改变任何东西,一个改变的请求(另一个调度、目标或有效负载)会替换挂起的请求 以相同的身份运行一旦端点注册,调用它,例如fromsetupRestate的registrationCallbackFn.getCronJob({restateURL, id}):作业的请求和下次运行,或null当它不存在时;cancelCronJob({restateURL, id}):取消挂起的运行,清除作业和 把它读回来:只有当 工作真的没了,不然就丢了。可以安全地重复,而且是为了一份从未存在过的工作。
Cron表达式是 croner 模式(五个字段,或者六个,用于测试)。将目标处理程序发送到
payload 作为它的输入,或者当没有输入时没有输入。
服务和注册端点
setupRestate({ restateAdminURL, serviceURL: 'https://agent-service.internal:9080', services }) 接受一个完整的URL
地址重述到达端点在。它被注册为给定的,并且端点侦听其端口(80或443)
URL没有或声明方案的默认值),因此方案是必需的(没有单独的主机或端口选项);
通过 port 当端点侦听URL中的另一个端口时(服务或代理后面的容器)。这个
运行状况端点在此之后侦听端口,除非 healthPort 是集。
它提供服务,注册部署,并在SIGINT/SIGTERM上再次删除它。正在运行的调用是
将它们固定在部署上,因此删除它们会束缚它们:重新启动的进程无法完成它们,它们只能
死亡。对于在同一地址重新启动的进程,通过 keepDeploymentOnShutdown: true 和 forceRegistration: true:
当相同的地址再次注册时(根据Restate 1.7进行检查),运行时,Restate保留部署ID
在新流程上继续调用,并且 force 更新现有的服务和处理程序
重新注册会保留旧的处理程序列表)。只有兼容的更改才安全:调用已删除的或
重命名的处理程序无法恢复。
在不注册的情况下服务:当其他东西拥有注册(一个只注册一次进程地址并重启进程的管理器)时,使用
serveEndpoint({port, services}) 相反,它只是服务。已经在部署上运行的调用被固定到它的
地址,因此部署必须在进程停止后继续运行,并返回到相同的地址。
塑造代理
defineAgent 基本选项之外的选项:
| 选项 | 做 |
|---|---|
tools 作为一个函数 | tools: (input) => ({...}) 根据已验证的请求为依赖于该请求的工具构建工具。它在任何 ctx.run 每次执行和重放,必须是纯粹的。无效映射最终导致请求失败(500)。 |
output 作为一个函数 | output: (input) => schema | undefined 为每个请求选择答案的JSON模式(规则与 tools:纯I/O); undefined 表示该请求的自由文本回答。与任何 output,模型的答案将根据模式进行检查:错误的答案将交给模型来纠正,而永远不会正确的模型将在 maxSteps. |
respond(result, input) | 形状是什么 ask 从循环结果返回 {text, output?, steps, totalTokens, trace}. 搭配 responseSchema,在Restate记录成形值之前对其进行验证。 |
label(input) | 然后读取终端故障 [label] message因此,一次失败的运行说明了它在做什么。保留代码和元数据;取消、超时和可重试错误不受影响。 |
limits.maxInputBytes | 最大请求(JSON字节,默认64 KB);较大的一个最终被拒绝(413)。 |
键控块工具。 块工具调用另一个重构构建块。它的 key(args, meta) 和 input(args, meta)
钩子从模型的参数和每次调用的幂等键中决定目标键和参数
(meta.idempotencyKey,对于每个工具调用都是唯一的)。对多个呼叫都相同的键(如: lock:<resource>)
让一个虚拟对象一次一个地运行这些调用,并将其余的持久地排队;由等幂键构建的键
使每个调用独立。 input 添加模型不能控制的内容,例如调用者或允许的文件夹。
两个钩子都不能在运行前抛出:validate,而不能在它们内部抛出。
工具调用的大小上限。 工具参数和结果被记录下来并重播,所以每个工具都有
maxInputBytes (默认16 KB)和 maxOutputBytes (默认32 KB)。过大的参数不能运行
模型被告知原因;过大的结果将被模型看到的错误所取代。对于一个 execute 工具的
检查发生在它自己的内部 ctx.run,所以大的值永远不会被记录。
多么好的记录啊。 每一个 llm-step-N 小日志 meta (model, finishReason, inputTokens,
outputTokens, durationMs),而不是提示或回复。循环返回与 trace
(每个步骤一个条目,包含所要求的工具名称),并记录一个 ctx.console 每一步行,保持不变
重播时沉默。
配置。 readLlmEnv(process.env) 返回 {LLM_BASE_URL, LLM_API_KEY?, MODEL_ID} 失败了
命名丢失的设置并且从不打印值的终端500。
测试代理。 @empyria/restate/testing 出口 fakeCtx (一个日志 run, genericCall,
awakeable,状态和捕获 console)和模型-结果帮助器 textResult, toolCallResult
和 sequence,无需加载AI SDK。
错误策略:最终(被拒绝或超时)批准失败的工具,被调用者的
TerminalError(执行工具重试失败)返回模型作为错误结果,
所以代理可以适应。暂时错误留给Restate重试和取消
总是传播。
没有官方的Restate Admin API客户端(直接针对已发布的API进行验证)
@restatedev/restate-sdk/-clients 包) lib/Admin.js 手写的包装纸保留了吗
与实时管理API的OpenAPI规范(<admin-url>/openapi)。
每个导出的函数都直接用JSDoc记录在其源文件中——悬停
VSCode或Zed中的函数显示其参数和返回类型,不需要任何额外的内容
工具,因为两个编辑器都从plain读取JSDoc .js 自动文件。
测试在 测试/,每个模块一个文件,与源文件分开。他们
模拟 fetch 和Restate SDK客户端,而不是需要一个实时服务器
例外是 setupRestate,它绑定实际端口并安装进程信号处理程序
并且故意没有经过测试(见 AGENTS.md)。
脚本
bun run format # check formatting (oxfmt)
bun run format:fix # apply formatting
bun run lint # lint (oxlint)
bun run lint:fix # lint and fix
bun run test # run tests with coverage
许可证
MIT©Imre Fazekas