@hostanagent/tool-runner 0.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 GeekArt Inc.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,11 @@
1
+ # @hostanagent/tool-runner 0.2.4
2
+
3
+ HostAnAgent 协议 1.0 的独立交付包。当前是本地候选产物,尚未公开发布。
4
+
5
+ 安装固定归档:`npm install ./hostanagent-tool-runner-0.2.4.tgz`。依赖 Client Core 的包需同时安装 `hostanagent-client-core-0.2.4.tgz`。
6
+
7
+ 完整接入指南随包提供在 `SDK_ONBOARDING.md`,包含服务端 Key、产品登录代理、恢复、工具与组件扩展;无需取得私有源码仓库权限即可阅读已安装包中的指南。
8
+
9
+ 公开入口:包根。Client Core 另有 Node 专用 `/bridge`;Widget 另有独立浏览器脚本 `/embed`(dist/agent-chat.js)。Client Core 包根无 DOM 依赖。浏览器只能连接产品登录代理,不持有应用/供应商 Key。挂载或恢复历史不自动安装 ToolRunner。
10
+
11
+ 本客户端包及随包文档采用 [MIT](https://opensource.org/license/mit),Copyright (c) 2026 GeekArt Inc.。第三方信息见 THIRD_PARTY_NOTICES.md。软件许可不包含托管服务或模型使用额度;尚未公开发布。
@@ -0,0 +1,125 @@
1
+ # HostAnAgent SDK / Widget 0.2.4 接入
2
+
3
+ 0.2.4 为 MIT 许可准备版本,尚未公开上传。包含 D07 诊断契约及 durable 执行结束分类;0.2.3 及更早历史产物保持不变。
4
+
5
+ 此版本交付本地产物和安装验证,尚未上传 npm、PyPI 或 CDN。四个客户端包及随包文档、示例采用 MIT,Copyright (c) 2026 GeekArt Inc.,完整文本随包提供在 LICENSE。软件授权范围为客户端接入代码,托管服务和模型使用额度另行提供。不要向供应商填写 Key,也不要把 HAA 应用 Key 放进浏览器。
6
+
7
+ ## 1. 准备 Agent 与服务端身份
8
+
9
+ 在控制台创建并发布 Agent,记下不可变版本 ID。模型访问与计费由平台管理,产品只实现自己的登录、业务权限与工具。
10
+
11
+ 当前代码已整合 D01 的自助受限 Key 与 `/v1/runtime/bootstrap`。创建限制到目标 Agent 的服务端 Key:文本接入需要 `runtime:read`、`runtime:write`;显式执行工具时增加 `tools:execute`。聊天框不需要 `configuration:read/write`。默认代理只消费运行发现,缺失时拒绝初始化,绝不回退读取完整配置。部署状态以文档仓库实施记录为准;下面的独立示例仍使用显式 mock,不连接线上账号。
12
+
13
+ 仅服务端设置:
14
+
15
+ ```text
16
+ HAA_BACKEND_URL=https://api.hostanagent.com
17
+ HAA_APPLICATION_KEY=<受限的 HostAnAgent 应用 Key>
18
+ PRODUCT_PUBLIC_ORIGIN=https://your-product.example
19
+ ```
20
+
21
+ MCP `https://api.hostanagent.com/mcp` 仍只用于配置,不是运行、余额或充值入口。
22
+
23
+ ## 2. 从本地产物安装并运行最小示例
24
+
25
+ 在发布目录之外新建目录,将三个 `.tgz` 和 `example/` 复制过来。安装命令使用具体文件名,不依赖仓库、源码别名、Vite 或 latest:
26
+
27
+ ```bash
28
+ npm install --save-exact ./hostanagent-client-core-0.2.4.tgz ./hostanagent-chat-widget-0.2.4.tgz ./hostanagent-tool-runner-0.2.4.tgz
29
+ node example/server.mjs
30
+ ```
31
+
32
+ 打开 `http://127.0.0.1:4176`,点击登录模拟用户后发送文本。此示例仅监听本机,使用明确标注的模拟登录、运行发现、模型和资产,没有实际供应商请求、自动入账或业务工具。`PORT` 可以覆盖端口。`/balance` 与 `/model` 演示零余额及模型关闭。
33
+
34
+ 真实产品使用发布目录 `templates/app-backend-bridge/product-server.mjs`(源码位于 `examples/app-backend-bridge/`),其依赖入口是已安装的 `@hostanagent/client-core/bridge`。把返回的 Node handler 挂在 `/api/agent/*`,静态文件和产品登录由你的服务处理。回调替换点:
35
+
36
+ - `authenticateProductUser(request)` 验证已有登录 cookie,返回稳定用户 ID 和本次登录会话 ID;不接收浏览器声称的 user_id。
37
+ - `canUseAgent(subject, agentId)` 查询实时业务 ACL;Agent/版本由运行发现提供。
38
+ - 可选 `canExecuteTool(subject, call, operation)` 校验工具名、版本、资源、参数摘要和操作权限。不挂载时所有 claim/receipts 都拒绝。
39
+ - 可选 `canUseExecutor(subject, {path, method, body})` 消费 D02 的执行器授权钩子。产品必须把稳定 executor ID 解析到自己的可信登记,再逐次检查 Agent、绑定、执行位置和当前权限;未传回调时所有连接、心跳、候选、报告和断开路径均拒绝。此许可不自动放开 claim/receipts。
40
+
41
+ 代理仅暴露运行及明确授权的执行器路径,按 Subject 读取会话/Run,再检查授权版本;身份变化时拒绝旧页面的 `X-Agent-Identity`。该值只是 bootstrap 返回的登录绑定,不能代替 cookie。每次请求重新读取发现与 ACL;停用/归档阻止所有版本的新 Run,允许有权限的历史读取。平台已强制检查 Key scope、Agent 范围与账号状态;本版已整合 D05:可通过 `/agents/{id}/runtime?version_id=...` 读取可启动状态;服务端在幂等查询后检查停用/归档,已接受 Run 的同 key 重试仍返回原结果。
42
+
43
+ ## 3. 文本 Run、思考强度与恢复
44
+
45
+ ```ts
46
+ import { AgentClient, bootstrap } from '@hostanagent/client-core';
47
+ const metadata = await bootstrap('/api/agent');
48
+ const client = new AgentClient({ endpoint: '/api/agent', agentVersion: '固定版本 ID', identityId: metadata.identity_id });
49
+ client.subscribe(state => { /* 从 state.run.messages 渲染流式正文;不要执行工具 */ });
50
+ await client.send('整理我的任务', { reasoning_effort: 'high', limits: { max_cost_usd: '1.000000000000' } });
51
+ ```
52
+
53
+ 五档 `low/medium/high/xhigh/max` 以 bootstrap 的模型 `reasoning_options` 为准,不支持的档位由平台拒绝。USD 始终是十进制字符串。零余额报 `INSUFFICIENT_BALANCE`,没有模型调用;目前测试入账需平台管理员人工处理,在线充值未开放。模型关闭/价格未就绪不自动换模型。
54
+
55
+ 网络断开、停止和未知结果的处理不同:
56
+
57
+ | 情况 | 处理 |
58
+ |---|---|
59
+ | SSE 中断/事件缺口 | 自动读原 Run 快照,从 `resume_cursor` 补读,不发送新 Run |
60
+ | 创建请求响应丢失 | 显式重试相同 `send` 内容,内存里冻结的幂等键与完整参数保持不变 |
61
+ | 已拿到 Run ID,但快照失败 | 重试相同内容只读该 Run,不再 POST 创建 |
62
+ | 页面重载/进程退出 | 默认不持久化正文;由产品记录已接受 Run ID 或原请求与幂等键后核对,不生成新键猜测重做 |
63
+ | 追加输入响应未知 | 使用原内容和原幂等键重试 `/inputs` |
64
+ | 用户停止 | `await client.cancel()`,以服务端状态为准;不是业务任务已取消的证明 |
65
+ | 收起/卸载/断开连接 | 仅释放视图/观察连接,不取消 Run |
66
+
67
+ 恢复已有 Session 用其冻结的 `agent_version` 构造 client,再 `restore(sessionId)`;恢复 Run 用 `connect(runId)`。Widget 带 `sessionId` 时会先读旧版本,绝不跟随最新发布。显式升级需要销毁旧视图并创建不带旧 sessionId 的新 Widget。身份切换先 `runner?.revoke()`,再 `client.resetIdentity()`、销毁旧视图,用新登录 bootstrap 重新挂载。
68
+
69
+ ## 4. 显式挂载工具
70
+
71
+ 文本 Widget 不导入或自动启动 ToolRunner。可以在产品确认执行权限后单独安装:
72
+
73
+ ```ts
74
+ import { AgentToolRunner, proxyCoordinator } from '@hostanagent/tool-runner';
75
+ const runner = AgentToolRunner.attach(client, {
76
+ executorId: productExecutorId,
77
+ coordinator: proxyCoordinator(client),
78
+ handlers: { 'product.read_selection': async () => product.readAuthorizedSelection() },
79
+ receipts: productReceiptStore,
80
+ authorize: (call, phase) => product.authorizeTool(call, phase),
81
+ reconcile: call => product.reconcileReceipt(call.id, call.arguments_digest),
82
+ onError: () => showReconciliationRequired(),
83
+ });
84
+ ```
85
+
86
+ 上面的 `product*` 都是接入方明确提供的对象,不是内置 API。0.2.4 包含 D02 的 `ExecutorConnection`、持久 assignment、`flushReceipts`、执行/核对授权与诊断接口;连接和心跳仍由产品显式调用,不自动启动执行器。参考发布目录 `templates/durable-tool-runner/README.md`(源码位于 `examples/durable-tool-runner/`),独立 Node 声明已包含全部入口。
87
+
88
+ 无副作用读取先验收,写工具再配持久回执、业务幂等和参数绑定审批。`replayed` claim 只允许重投已有回执或核对,不能重跑 handler。协调器的 receipt 必须返回 `{ accepted: true }` 才能继续;普通异常表示业务结果待核对,只有业务已确认失败/取消时才抛出 `BusinessFailure` / `BusinessCancelled`。handler 必须等待 progress 回执。新旧身份使用各自的 runner/client/store 实例;默认 MemoryReceiptStore 不提供跨进程持久化。真实 SitRep/AIAniGen 接入仍由对应产品验收。
89
+
90
+ ## 5. 固定脚本与组件扩展
91
+
92
+ 将发布目录 `vendor/agent-chat/0.2.4/agent-chat.js` 和同目录 LICENSE 原样复制到产品静态目录,核对 manifest 的 SHA-256 / SRI。脚本内也保留完整 MIT 声明;再次打包、压缩或分发时应保留该许可注释。该脚本包含 Client Core,没有外部 JS 依赖:
93
+
94
+ ```html
95
+ <div id="assistant" style="height:600px"></div>
96
+ <script src="/vendor/agent-chat/0.2.4/agent-chat.js"></script>
97
+ <script>
98
+ AgentChat.mount('#assistant', { endpoint: '/api/agent', agentId: '你的 Agent ID' });
99
+ </script>
100
+ ```
101
+
102
+ 严格 CSP 的产品将挂载代码放在自己的外部脚本中。传入明确 `agentVersion` 时保留旧接入的无 bootstrap 挂载行为;新产品代理需同时传入 `identityId: metadata.identity_id`(先调用 `bootstrap`),或直接传入已初始化的 `client`。也可 `import { mount } from '@hostanagent/chat-widget'`;模块依赖固定 `@hostanagent/client-core@0.2.4`。
103
+
104
+ 支持受控 Markdown(标题、段落、列表、引用、强调、代码块和复制)、安全链接、公开推理、工具/确认卡片;原始 HTML 是纯文本,不加载 Markdown 图片,不执行模型返回的脚本。`renderers` 是产品可信代码,产品负责它的安全性。通用工具 JSON 展示上限 16,000 字符,正文 Markdown 展示上限 200,000 字符。
105
+
106
+ `selectAttachments(): Promise<ReferenceCard[]>` 由产品选择/上传并授权资源;卡片的 `ref` 作为 `context_refs` 发送,图片和音频不会因此成为模型输入。活动 Run 的 `/inputs` 只接受文本,资源引用需下一轮发送。`getReferences(message)` 可从产品数据返回来源/资产卡片(title、description、url、ref);只允许 HTTP(S)、同源绝对路径或片段链接。不默认写 localStorage/IndexedDB。
107
+
108
+ ## 6. 发布与升级
109
+
110
+ | 产物 | 版本与入口 | 兼容边界 |
111
+ |---|---|---|
112
+ | Python `hostanagent-client` | 0.2.4;`from hostanagent import AgentClient` | Python >=3.10;协议 1.0;仅 httpx 运行依赖 |
113
+ | TS Client Core | 0.2.4;包根、`/bridge`(仅 Node) | 包根无 DOM;Node >=22.12;浏览器 fetch/Streams/crypto |
114
+ | ToolRunner | 0.2.4;包根 | 已整合 D02 连接与可靠回执语义;需显式协调器/handler |
115
+ | Widget | 0.2.4;包根、`/embed` | 协议 1.0;Shadow DOM;浏览器最终证据见交付记录 |
116
+
117
+ 0.2.0 增加完整 Run 参数、错误关联 ID、运行发现与 Node 代理、组件扩展,并修复接受成功后的重试重复运行风险。0.2.1 整合主分支 D01/D02/D03/D04,采用同一份规范协议产物,修复新增执行器类型的 NodeNext 导入,并提供 D02 同款显式执行器代理授权。升级 ToolRunner 时按上节核对回执确认、普通异常和业务失败语义。产品应成组升级 JS 三包和脚本,先在 mock 验证,再接真实已授权服务;保留旧 0.2.0/0.2.1 文件。0.2.2 已整合 D05 固定版本和停用规则;修复空会话恢复残留、批量 SSE 回放误判超限,以及发送完成时清除后续编辑或抢走阅读焦点的问题。未知请求未核对前,Client Core 拒绝切换其他 Session,仍使用原 key 恢复;身份切换仍明确丢弃旧身份状态。服务端协议仍为 1.0,生产联合验收另行进行。
118
+
119
+ 0.2.3 完成源码包、发布工作流与安装矩阵准备;0.2.4 补齐 GeekArt Inc. 的 MIT 声明及独立脚本许可注释。0.2.3 及更早产物保持原样。
120
+
121
+ 从明确提交构建:`pnpm release:pack`(要求干净 checkout);输出 `artifacts/0.2.4/manifest.json`、三包、固定脚本、示例和产品代理/持久回执模板。manifest 同时记录准确契约摘要、全部文件 SHA-256 和 SRI;安装/浏览器验证先核对完整清单。只复制 Git 跟踪的示例文件,不纳入示例目录内的本地环境文件或依赖目录。已有版本目录会拒绝覆盖。开发中可用 `pnpm release:pack --candidate` 生成独立候选目录并记录实际 source_dirty 状态。运行 `pnpm test:packages <产物目录>`,在系统临时目录通过 tarball 安装、严格 Node 无 DOM / 浏览器类型检查与执行。测试只复制第三方编译器类型工具,不链接任何项目源码。
122
+
123
+ MIT 许可见各包、示例和模板中的 LICENSE;第三方信息见各包 THIRD_PARTY_NOTICES.md。构建和校验不执行上传。另有 `scripts/publish-release.mjs <产物目录>` 核对可发布性与远端同版本摘要,只有显式加入 `--publish` 才上传;版权声明不完整时会在上传前停止。保留旧产物,核对包名权限、服务端集成和发布授权后再公开上传。Railway 仍按已提交源码手动部署。
124
+
125
+ 兼容窗口:0.2.0/0.2.1 的固定版本运行接口继续兼容当前协议 1.0;D02 执行器使用 0.2.1 起的接口,D05 元数据与逻辑 Agent 选择需要已整合 D05 的服务端。Python 便捷入口见 wheel 内 README。未来删除字段或改变运行语义必须先发布不兼容版本和迁移说明,不能覆盖同版本产物。当前仅提供自托管文件,不存在可承诺的公共 CDN URL。
@@ -0,0 +1,7 @@
1
+ # 许可与构建来源
2
+
3
+ 本客户端包及随包文档采用 MIT,Copyright (c) 2026 GeekArt Inc.,完整文本见 LICENSE。第三方依赖继续适用各自的许可证。
4
+
5
+ 发布的 JavaScript 只包含本仓库代码。Client Core 使用平台内置 fetch/Streams/crypto;Node bridge 使用 Node 内置模块。ToolRunner 和 Widget ES 模块精确依赖本项目 Client Core;Widget IIFE 将 Client Core 打入脚本。没有引入 Markdown/HTML 第三方解析器。
6
+
7
+ 开发构建工具为 TypeScript(Apache-2.0)、Vite / Rollup / esbuild(MIT),以及测试用 Vitest(MIT)和 Playwright(Apache-2.0)。它们不是包的运行依赖。开发工具及其依赖完整许可证保留在各自已安装的包中;如另行分发工具本身应一并保留对应许可。版本与完整性由仓库 pnpm-lock.yaml 固定。
@@ -0,0 +1,14 @@
1
+ import type { AgentClient, ExecutorConnect, ExecutorHeartbeat, ExecutorRecord, PendingToolCalls } from '@hostanagent/client-core';
2
+ import type { ExecutorDiagnostic } from './index.js';
3
+ /** Explicit lifecycle over a product-authorized bridge. No background execution or implicit mount. */
4
+ export declare class ExecutorConnection {
5
+ private client;
6
+ readonly registration: ExecutorConnect;
7
+ constructor(client: AgentClient, registration: ExecutorConnect);
8
+ connect(): Promise<ExecutorRecord>;
9
+ heartbeat(status?: ExecutorHeartbeat): Promise<ExecutorRecord>;
10
+ disconnect(): Promise<ExecutorRecord>;
11
+ pending(cursor?: string, limit?: number): Promise<PendingToolCalls>;
12
+ report(runId: string, toolCallId: string, state: ExecutorDiagnostic): Promise<unknown>;
13
+ private path;
14
+ }
@@ -0,0 +1,74 @@
1
+ import type { AgentClient, PublicToolCall, ReceiptCreate } from '@hostanagent/client-core';
2
+ export interface Assignment {
3
+ toolCall: PublicToolCall;
4
+ grant: string;
5
+ replayed: boolean;
6
+ }
7
+ export interface Coordinator {
8
+ claim(call: PublicToolCall, executorId: string): Promise<Assignment | null>;
9
+ receipt(assignment: Assignment, receipt: ReceiptCreate): Promise<unknown>;
10
+ }
11
+ export interface ReceiptStore {
12
+ get(callId: string): Promise<ReceiptCreate | undefined>;
13
+ put(callId: string, receipt: ReceiptCreate): Promise<void>;
14
+ remove(callId: string): Promise<void>;
15
+ /** Optional durable assignment journal. Persist before calling any business handler. */
16
+ prepare?(assignment: Assignment): Promise<void>;
17
+ pending?(): Promise<Assignment[]>;
18
+ }
19
+ export declare class MemoryReceiptStore implements ReceiptStore {
20
+ private items;
21
+ get(id: string): Promise<ReceiptCreate | undefined>;
22
+ put(id: string, value: ReceiptCreate): Promise<void>;
23
+ remove(id: string): Promise<void>;
24
+ }
25
+ export interface ToolContext {
26
+ toolCallId: string;
27
+ operationKey: string;
28
+ argumentsDigest: string;
29
+ signal: AbortSignal;
30
+ progress(message: string): Promise<void>;
31
+ }
32
+ export type ToolHandler = (arguments_: Record<string, unknown>, context: ToolContext) => Promise<unknown>;
33
+ /** Only use these when the business system has confirmed its final outcome. */
34
+ export declare class BusinessFailure extends Error {
35
+ }
36
+ export declare class BusinessCancelled extends Error {
37
+ }
38
+ export type ExecutorDiagnostic = 'reconciliation_required' | 'receipt_pending';
39
+ export interface RunnerOptions {
40
+ coordinator: Coordinator;
41
+ handlers: Record<string, ToolHandler>;
42
+ /** The application persists identity and business idempotency if recovery is required. */
43
+ executorId: string;
44
+ receipts?: ReceiptStore;
45
+ reconcile?: (call: PublicToolCall) => Promise<ReceiptCreate | undefined>;
46
+ authorize?: (call: PublicToolCall, phase: 'execute' | 'reconcile') => Promise<void>;
47
+ onDiagnostic?: (call: PublicToolCall, state: ExecutorDiagnostic) => void;
48
+ onError?: (call: PublicToolCall, error: unknown) => void;
49
+ }
50
+ export declare function proxyCoordinator(client: AgentClient): Coordinator;
51
+ /** Execution is opt-in and coordinated. Chat rendering never instantiates this class. */
52
+ export declare class AgentToolRunner {
53
+ private seen;
54
+ private active;
55
+ private busy;
56
+ private unsubscribe;
57
+ private receipts;
58
+ private options;
59
+ private client;
60
+ private stopped;
61
+ constructor(client: AgentClient, options: RunnerOptions);
62
+ static attach(client: AgentClient, options: RunnerOptions): AgentToolRunner;
63
+ private execute;
64
+ private deliver;
65
+ private unknown;
66
+ private reconcile;
67
+ /** Explicit recovery also delivers saved results for runs that have already stopped. */
68
+ flushReceipts(): Promise<void>;
69
+ /** Stops new assignments. In-flight business handlers decide how to finish. */
70
+ detach(): void;
71
+ /** Call when product identity changes; application authorization remains final. */
72
+ revoke(): void;
73
+ }
74
+ export { ExecutorConnection } from './connection.js';
package/dist/index.js ADDED
@@ -0,0 +1,225 @@
1
+ /*!
2
+ MIT License
3
+
4
+ Copyright (c) 2026 GeekArt Inc.
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ of this software and associated documentation files (the "Software"), to deal
8
+ in the Software without restriction, including without limitation the rights
9
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ copies of the Software, and to permit persons to whom the Software is
11
+ furnished to do so, subject to the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be included in all
14
+ copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ SOFTWARE.
23
+ */
24
+ class y {
25
+ constructor(t, e) {
26
+ this.client = t, this.registration = e;
27
+ }
28
+ client;
29
+ registration;
30
+ connect() {
31
+ return this.client.request("/executors/connect", { method: "POST", body: JSON.stringify(this.registration) });
32
+ }
33
+ heartbeat(t = {}) {
34
+ return this.client.request(this.path("heartbeat"), { method: "POST", body: JSON.stringify(t) });
35
+ }
36
+ disconnect() {
37
+ return this.client.request(this.path("disconnect"), { method: "POST" });
38
+ }
39
+ pending(t, e = 50) {
40
+ const i = new URLSearchParams({ limit: String(e), ...t ? { cursor: t } : {} });
41
+ return this.client.request(`${this.path("pending")}?${i}`);
42
+ }
43
+ report(t, e, i) {
44
+ return this.client.request(this.path("calls/report"), { method: "POST", body: JSON.stringify({ run_id: t, tool_call_id: e, state: i }) });
45
+ }
46
+ path(t) {
47
+ return `/executors/${encodeURIComponent(this.registration.executor_id)}/${t}`;
48
+ }
49
+ }
50
+ class f {
51
+ items = /* @__PURE__ */ new Map();
52
+ async get(t) {
53
+ return structuredClone(this.items.get(t));
54
+ }
55
+ async put(t, e) {
56
+ this.items.set(t, structuredClone(e));
57
+ }
58
+ async remove(t) {
59
+ this.items.delete(t);
60
+ }
61
+ }
62
+ class w extends Error {
63
+ }
64
+ class l extends Error {
65
+ }
66
+ function g(o) {
67
+ return {
68
+ async claim(t, e) {
69
+ try {
70
+ const i = await o.request(`/runs/${t.run_id}/tool-calls/${t.id}/claim`, { method: "POST", body: JSON.stringify({ executor_id: e }) });
71
+ return { toolCall: i.tool_call, grant: i.grant, replayed: i.replayed };
72
+ } catch (i) {
73
+ if (typeof i == "object" && i !== null && "status" in i && i.status === 409) return null;
74
+ throw i;
75
+ }
76
+ },
77
+ receipt(t, e) {
78
+ const i = t.toolCall;
79
+ return o.request(`/runs/${i.run_id}/tool-calls/${i.id}/receipts`, { method: "POST", headers: { "X-Tool-Grant": t.grant }, body: JSON.stringify(e) });
80
+ }
81
+ };
82
+ }
83
+ class h {
84
+ seen = /* @__PURE__ */ new Set();
85
+ active = /* @__PURE__ */ new Map();
86
+ busy = /* @__PURE__ */ new Set();
87
+ unsubscribe = null;
88
+ receipts;
89
+ options;
90
+ client;
91
+ stopped = !1;
92
+ constructor(t, e) {
93
+ this.client = t, this.options = e, this.receipts = e.receipts || new f();
94
+ }
95
+ static attach(t, e) {
96
+ const i = new h(t, e);
97
+ return i.unsubscribe = t.subscribe((r) => {
98
+ if (r.run)
99
+ for (const s of r.run.tool_calls)
100
+ s.run_id === r.run.id && (s.cancel_requested && i.active.get(s.id)?.abort(), !["completed", "failed", "cancelled", "expired"].includes(r.run.status) && ["ready", "claimed", "accepted"].includes(s.status) && e.handlers[s.name] && !i.seen.has(s.id) && (i.seen.add(s.id), i.execute(s)));
101
+ }), i;
102
+ }
103
+ async execute(t) {
104
+ if (!this.busy.has(t.id)) {
105
+ this.busy.add(t.id);
106
+ try {
107
+ const e = await this.options.coordinator.claim(t, this.options.executorId);
108
+ if (!e || this.stopped) return;
109
+ if (["id", "run_id", "binding_id", "name", "version", "arguments_digest"].some((n) => e.toolCall[n] !== t[n]))
110
+ throw new Error("分派内容与工具调用不一致");
111
+ t = e.toolCall;
112
+ const i = await this.receipts.get(t.id);
113
+ if (i && a(i)) {
114
+ await this.deliver(e, i);
115
+ return;
116
+ }
117
+ if (await this.receipts.prepare?.(e), e.replayed || i) {
118
+ await this.reconcile(e);
119
+ return;
120
+ }
121
+ const r = new AbortController();
122
+ this.active.set(t.id, r);
123
+ let s = 0;
124
+ const d = { receipt_id: crypto.randomUUID(), kind: "accepted" };
125
+ if (await this.receipts.put(t.id, d), await this.deliver(e, d), this.stopped) {
126
+ this.unknown(t);
127
+ return;
128
+ }
129
+ await this.options.authorize?.(t, "execute");
130
+ let c;
131
+ try {
132
+ const n = await this.options.handlers[t.name](t.arguments, {
133
+ toolCallId: t.id,
134
+ operationKey: t.id,
135
+ argumentsDigest: t.arguments_digest,
136
+ signal: r.signal,
137
+ progress: async (p) => {
138
+ const u = { receipt_id: crypto.randomUUID(), kind: "progress", progress_sequence: ++s, progress: p };
139
+ await this.receipts.put(t.id, u), await this.deliver(e, u);
140
+ }
141
+ });
142
+ c = { receipt_id: crypto.randomUUID(), kind: "completed", result: structuredClone(n) };
143
+ } catch (n) {
144
+ if (!(n instanceof w) && !(n instanceof l)) {
145
+ this.unknown(t);
146
+ return;
147
+ }
148
+ c = { receipt_id: crypto.randomUUID(), kind: n instanceof l ? "cancelled" : "failed", error: n.message };
149
+ }
150
+ await this.receipts.put(t.id, c), await this.deliver(e, c);
151
+ } catch (e) {
152
+ let i = !1;
153
+ try {
154
+ i = a(await this.receipts.get(t.id));
155
+ } catch {
156
+ }
157
+ this.options.onDiagnostic?.(t, i ? "receipt_pending" : "reconciliation_required"), this.options.onError?.(t, e);
158
+ } finally {
159
+ this.active.delete(t.id), this.busy.delete(t.id);
160
+ }
161
+ }
162
+ }
163
+ async deliver(t, e) {
164
+ e = structuredClone(e);
165
+ for (let i = 0; i < 3; i++)
166
+ try {
167
+ const r = await this.options.coordinator.receipt(t, e);
168
+ if (typeof r != "object" || r === null || !("accepted" in r) || r.accepted !== !0) throw new Error("回执尚未确认");
169
+ if (!a(e) && typeof r == "object" && r !== null && "late" in r && r.late) throw new Error("调用已停止或过期");
170
+ a(e) && await this.receipts.remove(t.toolCall.id);
171
+ return;
172
+ } catch (r) {
173
+ if (i === 2 || this.stopped) throw r;
174
+ await new Promise((s) => setTimeout(s, 250 * 2 ** i));
175
+ }
176
+ }
177
+ unknown(t) {
178
+ this.options.onDiagnostic?.(t, "reconciliation_required"), this.options.onError?.(t, new Error("执行结果需要接入方核对;不会自动重做操作"));
179
+ }
180
+ async reconcile(t) {
181
+ await this.options.authorize?.(t.toolCall, "reconcile");
182
+ const e = structuredClone(await this.options.reconcile?.(t.toolCall));
183
+ if (!e) {
184
+ this.unknown(t.toolCall);
185
+ return;
186
+ }
187
+ if (!a(e)) throw new Error("核对必须返回业务已确认的最终结果");
188
+ await this.receipts.put(t.toolCall.id, e), await this.deliver(t, e);
189
+ }
190
+ /** Explicit recovery also delivers saved results for runs that have already stopped. */
191
+ async flushReceipts() {
192
+ for (const t of await this.receipts.pending?.() || [])
193
+ if (!this.busy.has(t.toolCall.id)) {
194
+ this.busy.add(t.toolCall.id);
195
+ try {
196
+ const e = await this.receipts.get(t.toolCall.id);
197
+ e && a(e) ? await this.deliver(t, e) : await this.reconcile(t);
198
+ } catch (e) {
199
+ this.options.onError?.(t.toolCall, e);
200
+ } finally {
201
+ this.busy.delete(t.toolCall.id);
202
+ }
203
+ }
204
+ }
205
+ /** Stops new assignments. In-flight business handlers decide how to finish. */
206
+ detach() {
207
+ this.stopped = !0, this.unsubscribe?.(), this.unsubscribe = null;
208
+ }
209
+ /** Call when product identity changes; application authorization remains final. */
210
+ revoke() {
211
+ this.detach();
212
+ for (const t of this.active.values()) t.abort();
213
+ }
214
+ }
215
+ function a(o) {
216
+ return !!o && ["completed", "failed", "cancelled"].includes(o.kind);
217
+ }
218
+ export {
219
+ h as AgentToolRunner,
220
+ l as BusinessCancelled,
221
+ w as BusinessFailure,
222
+ y as ExecutorConnection,
223
+ f as MemoryReceiptStore,
224
+ g as proxyCoordinator
225
+ };
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "@hostanagent/tool-runner",
3
+ "version": "0.2.4",
4
+ "type": "module",
5
+ "description": "Explicit application-owned executor coordination",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist",
16
+ "README.md",
17
+ "THIRD_PARTY_NOTICES.md",
18
+ "SDK_ONBOARDING.md",
19
+ "LICENSE"
20
+ ],
21
+ "sideEffects": false,
22
+ "dependencies": {
23
+ "@hostanagent/client-core": "0.2.4"
24
+ },
25
+ "engines": {
26
+ "node": ">=22.12"
27
+ },
28
+ "repository": {
29
+ "type": "git",
30
+ "url": "https://github.com/ffskyfan/HostAnAgentWeb.git",
31
+ "directory": "packages/tool-runner"
32
+ },
33
+ "license": "MIT"
34
+ }