diff --git a/api/README.md b/api/README.md index 592dcef..3785a41 100644 --- a/api/README.md +++ b/api/README.md @@ -4,7 +4,7 @@ ## 公开范围 -10 组中英文页面覆盖接入认证、Agent Webhook 管理与触发、只读 Agent/Run/回执、HTTP + Socket.IO 对话、会话历史与消息、附件与媒体、模型代理、工作流与 OpenAPI 下载。35 个 HTTP 操作按服务分成三个自包含 OpenAPI 3.1 文件;对话事件和恢复规则维护在指南与可运行 Node.js 示例中。 +11 组中英文页面覆盖接入认证、Agent Webhook 管理与触发、只读 Agent/Run/回执、HTTP + Socket.IO 对话、会话历史与消息、附件与媒体、模型代理、工作流与 OpenAPI 下载。40 个 HTTP 操作按服务分成三个自包含 OpenAPI 3.1 文件;对话事件和恢复规则维护在指南与可运行 Node.js 示例中。 Webhook 所有者管理依赖握手 `capabilities.webhookManagement: true`,由可信入口选择实际工具并生成文件范围快照;旧服务仅支持已有事件。该支持条件没有被改写成未经核对的客户端发行号。现有 Write/Bash 与审批模型保留,不把配置文件宣称为不可修改的安全边界。 @@ -99,3 +99,10 @@ RunMeta 接受 `handed_off`,但现行 runtime-state HTTP 入口仍对该状态 最新核对源码为 `a101a40c14cc9d478c7f9a23d8a125ac2404509b`,内部文档指针为 `34178859da7a70cfab0b4726b85d9a37f22608a7`。本次维持35个公开HTTP操作,补齐敏感工具结果占位、引用移除、状态保留与客户端兼容处理说明,Agent Service规范描述修订为1.1.1;不把描述版本写成服务兼容性保证。详见[本次复核与验证](./stability-verification-2026-10-22.md)。 RunMeta五态与runtime-state HTTP四态/转交404的差异仍存在,公开文件继续按实际路由呈现。历史读取并非无损备份,不能从占位文字自动恢复执行。此前发布与搜索验收的记录保持各自历史基线,不将本次源码核对当作新生产或安装包验收。 + + +## 2026-10-25 观察与聚合历史扩展 + +本批新增输入配置、网络状态、聚合历史、近期工具调用和用量汇总5操作,Agent Service32、Compat3、Workflow5。新增双语实例诊断与用量页,扩展history all读取器及observe.mjs;observe维护文件与static下载逐字一致,校验器覆盖该镜像。源码ID与字段投影修复、旧索引过渡和本批定向验证见[本批记录](./observation-verification-2026-10-25.md)。 + +Worker两项未加入公开清单:独立隔离夹具确认其早期进度/旧结果重投影与entry游标说明仍需修复。该专项结果不扩写为跨用户泄漏或真实数据事故;见验证记录的范围说明。此前生产/搜索验收保留原始批次,不代表本批已部署。 diff --git a/api/endpoint-catalog.json b/api/endpoint-catalog.json index 966d380..e55261d 100644 --- a/api/endpoint-catalog.json +++ b/api/endpoint-catalog.json @@ -1,11 +1,11 @@ { "schemaVersion": 1, "reviewBaseline": { - "reviewedAt": "2026-10-22", + "reviewedAt": "2026-10-25", "clientRepository": "desirecore/desirecore", - "clientRevision": "a101a40c14cc9d478c7f9a23d8a125ac2404509b", - "internalDocsRevision": "34178859da7a70cfab0b4726b85d9a37f22608a7", - "publicDocsBaseRevision": "4530b1797db7bbe68961069cce599af1c7e5e258" + "clientRevision": "a0186f6feeaf66eb88787e8b80b8fea5559b8761", + "internalDocsRevision": "f8e1005d1cdfbeae37d3374de8631307b3d94534", + "publicDocsBaseRevision": "c76753613425d236d54360b111efb708b1349131" }, "pages": [ { @@ -57,6 +57,11 @@ "id": "api/attachments-and-media", "file": "docs/07-api/09-attachments-and-media.md", "slug": "/api/attachments-and-media" + }, + { + "id": "api/observability", + "file": "docs/07-api/10-observability.md", + "slug": "/api/observability" } ], "endpoints": [ @@ -75,7 +80,7 @@ "resultDelivery": "Synchronous JSON metadata. A 200 response verifies this authenticated request, not every optional capability.", "source": { "repository": "desirecore/desirecore", - "revision": "a101a40c14cc9d478c7f9a23d8a125ac2404509b", + "revision": "a0186f6feeaf66eb88787e8b80b8fea5559b8761", "files": [ "packages/agent-service/src/routes/client-handshake-routes.ts", "packages/agent-service/src/service-metadata.ts", @@ -92,7 +97,8 @@ "verification": { "record": "api/verification.md", "scope": "Fixed-source auth and handshake unit tests plus a real loopback HTTPS readiness integration; no public-domain or Caddy-container run.", - "baselineNote": "Source contract reviewed at a101a40c on 2026-10-22. Historical test results retain their own recorded baseline; current validation is recorded in api/stability-verification-2026-10-22.md." + "baselineNote": "Source contract reviewed at a101a40c on 2026-10-22. Historical test results retain their own recorded baseline; current validation is recorded in api/stability-verification-2026-10-22.md.", + "currentReview": "Static contract recheck at a0186f6feeaf66eb88787e8b80b8fea5559b8761 on 2026-10-25; historical executed tests retain their recorded baseline. Current observation checks are in api/observation-verification-2026-10-25.md." }, "spec": "static/openapi/agent-service.json", "securitySchemes": [ @@ -114,7 +120,7 @@ "resultDelivery": "200 {accepted:true,agentId,event} acknowledges receipt, with no Run ID or delivery deduplication. Only explicit authorized SendUserMessage output is delivered to the user; final model answers are not automatically returned to the caller or added to chat.", "source": { "repository": "desirecore/desirecore", - "revision": "a101a40c14cc9d478c7f9a23d8a125ac2404509b", + "revision": "a0186f6feeaf66eb88787e8b80b8fea5559b8761", "files": [ "packages/agent-service/src/routes/webhook-routes.ts", "packages/agent-service/src/routes/webhook-state.ts", @@ -133,7 +139,8 @@ "verification": { "record": "api/verification.md", "scope": "Real production owner management, atomic AgentFS snapshot writes, local fake-provider runtime and SendUserMessage delivery, rotation, failure and disable/delete integration. No commercial-provider or packaged-client claim.", - "baselineNote": "Source contract reviewed at a101a40c on 2026-10-22. Historical test results retain their own recorded baseline; current validation is recorded in api/stability-verification-2026-10-22.md." + "baselineNote": "Source contract reviewed at a101a40c on 2026-10-22. Historical test results retain their own recorded baseline; current validation is recorded in api/stability-verification-2026-10-22.md.", + "currentReview": "Static contract recheck at a0186f6feeaf66eb88787e8b80b8fea5559b8761 on 2026-10-25; historical executed tests retain their recorded baseline. Current observation checks are in api/observation-verification-2026-10-25.md." }, "spec": "static/openapi/agent-service.json", "securitySchemes": [ @@ -154,7 +161,7 @@ "resultDelivery": "Synchronous read; live state is observational, not a query completion signal.", "source": { "repository": "desirecore/desirecore", - "revision": "a101a40c14cc9d478c7f9a23d8a125ac2404509b", + "revision": "a0186f6feeaf66eb88787e8b80b8fea5559b8761", "files": [ "packages/agent-service/src/routes/agent-routes.ts", "packages/agent-service/src/agent/list-projection.ts" @@ -168,7 +175,8 @@ "verification": { "record": "api/verification.md", "scope": "Fixed-source request/response examples and focused HTTP + Socket.IO client contract; runtime fake in the public client fixture.", - "baselineNote": "Source contract reviewed at a101a40c on 2026-10-22. Historical test results retain their own recorded baseline; current validation is recorded in api/stability-verification-2026-10-22.md." + "baselineNote": "Source contract reviewed at a101a40c on 2026-10-22. Historical test results retain their own recorded baseline; current validation is recorded in api/stability-verification-2026-10-22.md.", + "currentReview": "Static contract recheck at a0186f6feeaf66eb88787e8b80b8fea5559b8761 on 2026-10-25; historical executed tests retain their recorded baseline. Current observation checks are in api/observation-verification-2026-10-25.md." }, "spec": "static/openapi/agent-service.json", "securitySchemes": [ @@ -189,7 +197,7 @@ "resultDelivery": "Synchronous read; live state is observational, not a query completion signal.", "source": { "repository": "desirecore/desirecore", - "revision": "a101a40c14cc9d478c7f9a23d8a125ac2404509b", + "revision": "a0186f6feeaf66eb88787e8b80b8fea5559b8761", "files": [ "packages/agent-service/src/routes/runs-routes.ts", "packages/agent-service/src/session/run-manager.ts" @@ -203,7 +211,8 @@ "verification": { "record": "api/history-attachments-verification.md", "scope": "Fixed-source request/response examples and focused HTTP + Socket.IO client contract; runtime fake in the public client fixture.", - "baselineNote": "Source contract reviewed at a101a40c on 2026-10-22. Historical test results retain their own recorded baseline; current validation is recorded in api/stability-verification-2026-10-22.md." + "baselineNote": "Source contract reviewed at a101a40c on 2026-10-22. Historical test results retain their own recorded baseline; current validation is recorded in api/stability-verification-2026-10-22.md.", + "currentReview": "Static contract recheck at a0186f6feeaf66eb88787e8b80b8fea5559b8761 on 2026-10-25; historical executed tests retain their recorded baseline. Current observation checks are in api/observation-verification-2026-10-25.md." }, "spec": "static/openapi/agent-service.json", "securitySchemes": [ @@ -224,7 +233,7 @@ "resultDelivery": "Synchronous read; live state is observational, not a query completion signal.", "source": { "repository": "desirecore/desirecore", - "revision": "a101a40c14cc9d478c7f9a23d8a125ac2404509b", + "revision": "a0186f6feeaf66eb88787e8b80b8fea5559b8761", "files": [ "packages/agent-service/src/routes/runs-routes.ts", "packages/agent-service/src/session/run-meta-read.ts" @@ -238,7 +247,8 @@ "verification": { "record": "api/history-attachments-verification.md", "scope": "Fixed-source request/response examples and focused HTTP + Socket.IO client contract; runtime fake in the public client fixture.", - "baselineNote": "Source contract reviewed at a101a40c on 2026-10-22. Historical test results retain their own recorded baseline; current validation is recorded in api/stability-verification-2026-10-22.md." + "baselineNote": "Source contract reviewed at a101a40c on 2026-10-22. Historical test results retain their own recorded baseline; current validation is recorded in api/stability-verification-2026-10-22.md.", + "currentReview": "Static contract recheck at a0186f6feeaf66eb88787e8b80b8fea5559b8761 on 2026-10-25; historical executed tests retain their recorded baseline. Current observation checks are in api/observation-verification-2026-10-25.md." }, "spec": "static/openapi/agent-service.json", "securitySchemes": [ @@ -259,7 +269,7 @@ "resultDelivery": "Synchronous read; live state is observational, not a query completion signal.", "source": { "repository": "desirecore/desirecore", - "revision": "a101a40c14cc9d478c7f9a23d8a125ac2404509b", + "revision": "a0186f6feeaf66eb88787e8b80b8fea5559b8761", "files": [ "packages/agent-service/src/routes/runs-routes.ts" ] @@ -272,7 +282,8 @@ "verification": { "record": "api/history-attachments-verification.md", "scope": "Fixed-source request/response examples and focused HTTP + Socket.IO client contract; runtime fake in the public client fixture.", - "baselineNote": "Source contract reviewed at a101a40c on 2026-10-22. Historical test results retain their own recorded baseline; current validation is recorded in api/stability-verification-2026-10-22.md." + "baselineNote": "Source contract reviewed at a101a40c on 2026-10-22. Historical test results retain their own recorded baseline; current validation is recorded in api/stability-verification-2026-10-22.md.", + "currentReview": "Static contract recheck at a0186f6feeaf66eb88787e8b80b8fea5559b8761 on 2026-10-25; historical executed tests retain their recorded baseline. Current observation checks are in api/observation-verification-2026-10-25.md." }, "spec": "static/openapi/agent-service.json", "securitySchemes": [ @@ -293,7 +304,7 @@ "resultDelivery": "Synchronous read; live state is observational, not a query completion signal.", "source": { "repository": "desirecore/desirecore", - "revision": "a101a40c14cc9d478c7f9a23d8a125ac2404509b", + "revision": "a0186f6feeaf66eb88787e8b80b8fea5559b8761", "files": [ "packages/agent-service/src/routes/runs-routes.ts", "packages/agent-service/src/receipts/reader.ts" @@ -307,7 +318,8 @@ "verification": { "record": "api/verification.md", "scope": "Fixed-source request/response examples and focused HTTP + Socket.IO client contract; runtime fake in the public client fixture.", - "baselineNote": "Source contract reviewed at a101a40c on 2026-10-22. Historical test results retain their own recorded baseline; current validation is recorded in api/stability-verification-2026-10-22.md." + "baselineNote": "Source contract reviewed at a101a40c on 2026-10-22. Historical test results retain their own recorded baseline; current validation is recorded in api/stability-verification-2026-10-22.md.", + "currentReview": "Static contract recheck at a0186f6feeaf66eb88787e8b80b8fea5559b8761 on 2026-10-25; historical executed tests retain their recorded baseline. Current observation checks are in api/observation-verification-2026-10-25.md." }, "spec": "static/openapi/agent-service.json", "securitySchemes": [ @@ -328,7 +340,7 @@ "resultDelivery": "HTTP admission or lookup JSON; query completion and incremental messages arrive through correlated Socket.IO events.", "source": { "repository": "desirecore/desirecore", - "revision": "a101a40c14cc9d478c7f9a23d8a125ac2404509b", + "revision": "a0186f6feeaf66eb88787e8b80b8fea5559b8761", "files": [ "packages/agent-service/src/routes/query-routes.ts", "packages/agent-service/src/query/client-request-registry.ts" @@ -342,7 +354,8 @@ "verification": { "record": "api/verification.md", "scope": "Fixed-source request/response examples and focused HTTP + Socket.IO client contract; runtime fake in the public client fixture.", - "baselineNote": "Source contract reviewed at a101a40c on 2026-10-22. Historical test results retain their own recorded baseline; current validation is recorded in api/stability-verification-2026-10-22.md." + "baselineNote": "Source contract reviewed at a101a40c on 2026-10-22. Historical test results retain their own recorded baseline; current validation is recorded in api/stability-verification-2026-10-22.md.", + "currentReview": "Static contract recheck at a0186f6feeaf66eb88787e8b80b8fea5559b8761 on 2026-10-25; historical executed tests retain their recorded baseline. Current observation checks are in api/observation-verification-2026-10-25.md." }, "spec": "static/openapi/agent-service.json", "securitySchemes": [ @@ -363,7 +376,7 @@ "resultDelivery": "HTTP admission or lookup JSON; query completion and incremental messages arrive through correlated Socket.IO events.", "source": { "repository": "desirecore/desirecore", - "revision": "a101a40c14cc9d478c7f9a23d8a125ac2404509b", + "revision": "a0186f6feeaf66eb88787e8b80b8fea5559b8761", "files": [ "packages/agent-service/src/routes/query-routes.ts", "packages/agent-service/src/query/client-request-registry.ts" @@ -377,7 +390,8 @@ "verification": { "record": "api/verification.md", "scope": "Fixed-source request/response examples and focused HTTP + Socket.IO client contract; runtime fake in the public client fixture.", - "baselineNote": "Source contract reviewed at a101a40c on 2026-10-22. Historical test results retain their own recorded baseline; current validation is recorded in api/stability-verification-2026-10-22.md." + "baselineNote": "Source contract reviewed at a101a40c on 2026-10-22. Historical test results retain their own recorded baseline; current validation is recorded in api/stability-verification-2026-10-22.md.", + "currentReview": "Static contract recheck at a0186f6feeaf66eb88787e8b80b8fea5559b8761 on 2026-10-25; historical executed tests retain their recorded baseline. Current observation checks are in api/observation-verification-2026-10-25.md." }, "spec": "static/openapi/agent-service.json", "securitySchemes": [ @@ -398,7 +412,7 @@ "resultDelivery": "HTTP admission or lookup JSON; query completion and incremental messages arrive through correlated Socket.IO events.", "source": { "repository": "desirecore/desirecore", - "revision": "a101a40c14cc9d478c7f9a23d8a125ac2404509b", + "revision": "a0186f6feeaf66eb88787e8b80b8fea5559b8761", "files": [ "packages/agent-service/src/routes/query-routes.ts", "packages/agent-service/src/query/client-request-registry.ts" @@ -412,7 +426,8 @@ "verification": { "record": "api/verification.md", "scope": "Fixed-source request/response examples and focused HTTP + Socket.IO client contract; runtime fake in the public client fixture.", - "baselineNote": "Source contract reviewed at a101a40c on 2026-10-22. Historical test results retain their own recorded baseline; current validation is recorded in api/stability-verification-2026-10-22.md." + "baselineNote": "Source contract reviewed at a101a40c on 2026-10-22. Historical test results retain their own recorded baseline; current validation is recorded in api/stability-verification-2026-10-22.md.", + "currentReview": "Static contract recheck at a0186f6feeaf66eb88787e8b80b8fea5559b8761 on 2026-10-25; historical executed tests retain their recorded baseline. Current observation checks are in api/observation-verification-2026-10-25.md." }, "spec": "static/openapi/agent-service.json", "securitySchemes": [ @@ -433,7 +448,7 @@ "resultDelivery": "Synchronous configuration or explicit delivery JSON. First configuration and rotation disclose a secret once; GET does not return it.", "source": { "repository": "desirecore/desirecore", - "revision": "a101a40c14cc9d478c7f9a23d8a125ac2404509b", + "revision": "a0186f6feeaf66eb88787e8b80b8fea5559b8761", "files": [ "packages/agent-service/src/routes/webhook-management-routes.ts", "packages/agent-service/src/auth/governed-endpoint-auth.ts" @@ -447,7 +462,8 @@ "verification": { "record": "api/verification.md", "scope": "Production Webhook management integration with local fake model upstream and real delivery persistence; no commercial model or desktop package verification.", - "baselineNote": "Source contract reviewed at a101a40c on 2026-10-22. Historical test results retain their own recorded baseline; current validation is recorded in api/stability-verification-2026-10-22.md." + "baselineNote": "Source contract reviewed at a101a40c on 2026-10-22. Historical test results retain their own recorded baseline; current validation is recorded in api/stability-verification-2026-10-22.md.", + "currentReview": "Static contract recheck at a0186f6feeaf66eb88787e8b80b8fea5559b8761 on 2026-10-25; historical executed tests retain their recorded baseline. Current observation checks are in api/observation-verification-2026-10-25.md." }, "spec": "static/openapi/agent-service.json", "securitySchemes": [ @@ -468,7 +484,7 @@ "resultDelivery": "Synchronous configuration or explicit delivery JSON. First configuration and rotation disclose a secret once; GET does not return it.", "source": { "repository": "desirecore/desirecore", - "revision": "a101a40c14cc9d478c7f9a23d8a125ac2404509b", + "revision": "a0186f6feeaf66eb88787e8b80b8fea5559b8761", "files": [ "packages/agent-service/src/routes/webhook-management-routes.ts", "packages/agent-service/src/auth/governed-endpoint-auth.ts" @@ -482,7 +498,8 @@ "verification": { "record": "api/verification.md", "scope": "Production Webhook management integration with local fake model upstream and real delivery persistence; no commercial model or desktop package verification.", - "baselineNote": "Source contract reviewed at a101a40c on 2026-10-22. Historical test results retain their own recorded baseline; current validation is recorded in api/stability-verification-2026-10-22.md." + "baselineNote": "Source contract reviewed at a101a40c on 2026-10-22. Historical test results retain their own recorded baseline; current validation is recorded in api/stability-verification-2026-10-22.md.", + "currentReview": "Static contract recheck at a0186f6feeaf66eb88787e8b80b8fea5559b8761 on 2026-10-25; historical executed tests retain their recorded baseline. Current observation checks are in api/observation-verification-2026-10-25.md." }, "spec": "static/openapi/agent-service.json", "securitySchemes": [ @@ -503,7 +520,7 @@ "resultDelivery": "Synchronous configuration or explicit delivery JSON. First configuration and rotation disclose a secret once; GET does not return it.", "source": { "repository": "desirecore/desirecore", - "revision": "a101a40c14cc9d478c7f9a23d8a125ac2404509b", + "revision": "a0186f6feeaf66eb88787e8b80b8fea5559b8761", "files": [ "packages/agent-service/src/routes/webhook-management-routes.ts", "packages/agent-service/src/auth/governed-endpoint-auth.ts" @@ -517,7 +534,8 @@ "verification": { "record": "api/verification.md", "scope": "Production Webhook management integration with local fake model upstream and real delivery persistence; no commercial model or desktop package verification.", - "baselineNote": "Source contract reviewed at a101a40c on 2026-10-22. Historical test results retain their own recorded baseline; current validation is recorded in api/stability-verification-2026-10-22.md." + "baselineNote": "Source contract reviewed at a101a40c on 2026-10-22. Historical test results retain their own recorded baseline; current validation is recorded in api/stability-verification-2026-10-22.md.", + "currentReview": "Static contract recheck at a0186f6feeaf66eb88787e8b80b8fea5559b8761 on 2026-10-25; historical executed tests retain their recorded baseline. Current observation checks are in api/observation-verification-2026-10-25.md." }, "spec": "static/openapi/agent-service.json", "securitySchemes": [ @@ -538,7 +556,7 @@ "resultDelivery": "Synchronous configuration or explicit delivery JSON. First configuration and rotation disclose a secret once; GET does not return it.", "source": { "repository": "desirecore/desirecore", - "revision": "a101a40c14cc9d478c7f9a23d8a125ac2404509b", + "revision": "a0186f6feeaf66eb88787e8b80b8fea5559b8761", "files": [ "packages/agent-service/src/routes/webhook-management-routes.ts", "packages/agent-service/src/auth/governed-endpoint-auth.ts" @@ -552,7 +570,8 @@ "verification": { "record": "api/verification.md", "scope": "Production Webhook management integration with local fake model upstream and real delivery persistence; no commercial model or desktop package verification.", - "baselineNote": "Source contract reviewed at a101a40c on 2026-10-22. Historical test results retain their own recorded baseline; current validation is recorded in api/stability-verification-2026-10-22.md." + "baselineNote": "Source contract reviewed at a101a40c on 2026-10-22. Historical test results retain their own recorded baseline; current validation is recorded in api/stability-verification-2026-10-22.md.", + "currentReview": "Static contract recheck at a0186f6feeaf66eb88787e8b80b8fea5559b8761 on 2026-10-25; historical executed tests retain their recorded baseline. Current observation checks are in api/observation-verification-2026-10-25.md." }, "spec": "static/openapi/agent-service.json", "securitySchemes": [ @@ -573,7 +592,7 @@ "resultDelivery": "Synchronous configuration or explicit delivery JSON. First configuration and rotation disclose a secret once; GET does not return it.", "source": { "repository": "desirecore/desirecore", - "revision": "a101a40c14cc9d478c7f9a23d8a125ac2404509b", + "revision": "a0186f6feeaf66eb88787e8b80b8fea5559b8761", "files": [ "packages/agent-service/src/routes/webhook-management-routes.ts", "packages/agent-service/src/auth/governed-endpoint-auth.ts" @@ -587,7 +606,8 @@ "verification": { "record": "api/verification.md", "scope": "Production Webhook management integration with local fake model upstream and real delivery persistence; no commercial model or desktop package verification.", - "baselineNote": "Source contract reviewed at a101a40c on 2026-10-22. Historical test results retain their own recorded baseline; current validation is recorded in api/stability-verification-2026-10-22.md." + "baselineNote": "Source contract reviewed at a101a40c on 2026-10-22. Historical test results retain their own recorded baseline; current validation is recorded in api/stability-verification-2026-10-22.md.", + "currentReview": "Static contract recheck at a0186f6feeaf66eb88787e8b80b8fea5559b8761 on 2026-10-25; historical executed tests retain their recorded baseline. Current observation checks are in api/observation-verification-2026-10-25.md." }, "spec": "static/openapi/agent-service.json", "securitySchemes": [ @@ -608,7 +628,7 @@ "resultDelivery": "Synchronous {items,total,limit,offset}; default limit50, maximum200. Items may include other authorized background deliveries.", "source": { "repository": "desirecore/desirecore", - "revision": "a101a40c14cc9d478c7f9a23d8a125ac2404509b", + "revision": "a0186f6feeaf66eb88787e8b80b8fea5559b8761", "files": [ "packages/agent-service/src/routes/schedule-routes.ts", "packages/agent-service/src/auth/governed-endpoint-auth.ts" @@ -620,7 +640,8 @@ "verification": { "record": "api/verification.md", "scope": "Production Webhook management integration with local fake model upstream and real delivery persistence; no commercial model or desktop package verification.", - "baselineNote": "Source contract reviewed at a101a40c on 2026-10-22. Historical test results retain their own recorded baseline; current validation is recorded in api/stability-verification-2026-10-22.md." + "baselineNote": "Source contract reviewed at a101a40c on 2026-10-22. Historical test results retain their own recorded baseline; current validation is recorded in api/stability-verification-2026-10-22.md.", + "currentReview": "Static contract recheck at a0186f6feeaf66eb88787e8b80b8fea5559b8761 on 2026-10-25; historical executed tests retain their recorded baseline. Current observation checks are in api/observation-verification-2026-10-25.md." }, "spec": "static/openapi/agent-service.json", "securitySchemes": [ @@ -914,7 +935,7 @@ "resultDelivery": "Complete persisted attachment receipt.", "source": { "repository": "desirecore/desirecore", - "revision": "a101a40c14cc9d478c7f9a23d8a125ac2404509b", + "revision": "a0186f6feeaf66eb88787e8b80b8fea5559b8761", "files": [ "packages/agent-service/src/routes/attachment-routes.ts", "packages/schemas/src/agent-service/attachments.ts", @@ -930,7 +951,8 @@ "verification": { "record": "api/history-attachments-verification.md", "scope": "Fixed-source route/schema/auth review. Standalone uploader: 12 local HTTP contract tests with a fake server, not production storage or model execution. Root records actual fixed-source route validation separately.", - "baselineNote": "Source contract reviewed at a101a40c on 2026-10-22. Historical test results retain their own recorded baseline; current validation is recorded in api/stability-verification-2026-10-22.md." + "baselineNote": "Source contract reviewed at a101a40c on 2026-10-22. Historical test results retain their own recorded baseline; current validation is recorded in api/stability-verification-2026-10-22.md.", + "currentReview": "Static contract recheck at a0186f6feeaf66eb88787e8b80b8fea5559b8761 on 2026-10-25; historical executed tests retain their recorded baseline. Current observation checks are in api/observation-verification-2026-10-25.md." }, "spec": "static/openapi/agent-service.json", "securitySchemes": [ @@ -952,7 +974,7 @@ "resultDelivery": "Original file bytes with no-store and nosniff.", "source": { "repository": "desirecore/desirecore", - "revision": "a101a40c14cc9d478c7f9a23d8a125ac2404509b", + "revision": "a0186f6feeaf66eb88787e8b80b8fea5559b8761", "files": [ "packages/agent-service/src/routes/attachment-routes.ts", "packages/schemas/src/agent-service/attachments.ts", @@ -966,7 +988,8 @@ "verification": { "record": "api/history-attachments-verification.md", "scope": "Fixed-source route/schema/auth review. Standalone uploader: 12 local HTTP contract tests with a fake server, not production storage or model execution. Root records actual fixed-source route validation separately.", - "baselineNote": "Source contract reviewed at a101a40c on 2026-10-22. Historical test results retain their own recorded baseline; current validation is recorded in api/stability-verification-2026-10-22.md." + "baselineNote": "Source contract reviewed at a101a40c on 2026-10-22. Historical test results retain their own recorded baseline; current validation is recorded in api/stability-verification-2026-10-22.md.", + "currentReview": "Static contract recheck at a0186f6feeaf66eb88787e8b80b8fea5559b8761 on 2026-10-25; historical executed tests retain their recorded baseline. Current observation checks are in api/observation-verification-2026-10-25.md." }, "spec": "static/openapi/agent-service.json", "securitySchemes": [ @@ -988,7 +1011,7 @@ "resultDelivery": "Direct media metadata.", "source": { "repository": "desirecore/desirecore", - "revision": "a101a40c14cc9d478c7f9a23d8a125ac2404509b", + "revision": "a0186f6feeaf66eb88787e8b80b8fea5559b8761", "files": [ "packages/agent-service/src/routes/media-routes.ts", "packages/agent-service/src/media/media-store.ts", @@ -1003,7 +1026,8 @@ "verification": { "record": "api/history-attachments-verification.md", "scope": "Fixed-source route/schema/auth review. Standalone uploader: 12 local HTTP contract tests with a fake server, not production storage or model execution. Root records actual fixed-source route validation separately.", - "baselineNote": "Source contract reviewed at a101a40c on 2026-10-22. Historical test results retain their own recorded baseline; current validation is recorded in api/stability-verification-2026-10-22.md." + "baselineNote": "Source contract reviewed at a101a40c on 2026-10-22. Historical test results retain their own recorded baseline; current validation is recorded in api/stability-verification-2026-10-22.md.", + "currentReview": "Static contract recheck at a0186f6feeaf66eb88787e8b80b8fea5559b8761 on 2026-10-25; historical executed tests retain their recorded baseline. Current observation checks are in api/observation-verification-2026-10-25.md." }, "spec": "static/openapi/agent-service.json", "securitySchemes": [ @@ -1025,7 +1049,7 @@ "resultDelivery": "Links and missing IDs; no universal success/data wrapper.", "source": { "repository": "desirecore/desirecore", - "revision": "a101a40c14cc9d478c7f9a23d8a125ac2404509b", + "revision": "a0186f6feeaf66eb88787e8b80b8fea5559b8761", "files": [ "packages/agent-service/src/routes/media-routes.ts", "packages/agent-service/src/media/media-store.ts", @@ -1042,7 +1066,8 @@ "verification": { "record": "api/history-attachments-verification.md", "scope": "Fixed-source route/schema/auth review. Standalone uploader: 12 local HTTP contract tests with a fake server, not production storage or model execution. Root records actual fixed-source route validation separately.", - "baselineNote": "Source contract reviewed at a101a40c on 2026-10-22. Historical test results retain their own recorded baseline; current validation is recorded in api/stability-verification-2026-10-22.md." + "baselineNote": "Source contract reviewed at a101a40c on 2026-10-22. Historical test results retain their own recorded baseline; current validation is recorded in api/stability-verification-2026-10-22.md.", + "currentReview": "Static contract recheck at a0186f6feeaf66eb88787e8b80b8fea5559b8761 on 2026-10-25; historical executed tests retain their recorded baseline. Current observation checks are in api/observation-verification-2026-10-25.md." }, "spec": "static/openapi/agent-service.json", "securitySchemes": [ @@ -1064,7 +1089,7 @@ "resultDelivery": "Full media bytes.", "source": { "repository": "desirecore/desirecore", - "revision": "a101a40c14cc9d478c7f9a23d8a125ac2404509b", + "revision": "a0186f6feeaf66eb88787e8b80b8fea5559b8761", "files": [ "packages/agent-service/src/routes/media-routes.ts", "packages/agent-service/src/media/media-store.ts", @@ -1083,7 +1108,8 @@ "verification": { "record": "api/history-attachments-verification.md", "scope": "Fixed-source route/schema/auth review. Standalone uploader: 12 local HTTP contract tests with a fake server, not production storage or model execution. Root records actual fixed-source route validation separately.", - "baselineNote": "Source contract reviewed at a101a40c on 2026-10-22. Historical test results retain their own recorded baseline; current validation is recorded in api/stability-verification-2026-10-22.md." + "baselineNote": "Source contract reviewed at a101a40c on 2026-10-22. Historical test results retain their own recorded baseline; current validation is recorded in api/stability-verification-2026-10-22.md.", + "currentReview": "Static contract recheck at a0186f6feeaf66eb88787e8b80b8fea5559b8761 on 2026-10-25; historical executed tests retain their recorded baseline. Current observation checks are in api/observation-verification-2026-10-25.md." }, "spec": "static/openapi/agent-service.json", "securitySchemes": [ @@ -1104,7 +1130,7 @@ "resultDelivery": "Lists ordinary user-visible history by updatedAt descending. Excludes delegated, manual and dedicated schedule/Webhook/Mention timelines. Preserves fork generations; do not concatenate branches as a deduplicated transcript. Pagination is not an immutable snapshot.", "source": { "repository": "desirecore/desirecore", - "revision": "a101a40c14cc9d478c7f9a23d8a125ac2404509b", + "revision": "a0186f6feeaf66eb88787e8b80b8fea5559b8761", "files": [ "packages/agent-service/src/routes/history-routes.ts", "packages/agent-service/src/session/store.ts", @@ -1120,7 +1146,8 @@ "verification": { "record": "api/history-attachments-verification.md", "scope": "Fixed-source route/owner/pagination review; real local source history routes and SessionStore fixture verifies authentication, owner isolation, public projection, search validation and empty/404 matter reads. The independent example uses 13 local HTTP contract tests, including paging gaps and truncation; no deployed-instance claim.", - "baselineNote": "Source contract reviewed at a101a40c on 2026-10-22. Historical test results retain their own recorded baseline; current validation is recorded in api/stability-verification-2026-10-22.md." + "baselineNote": "Source contract reviewed at a101a40c on 2026-10-22. Historical test results retain their own recorded baseline; current validation is recorded in api/stability-verification-2026-10-22.md.", + "currentReview": "Static contract recheck at a0186f6feeaf66eb88787e8b80b8fea5559b8761 on 2026-10-25; historical executed tests retain their recorded baseline. Current observation checks are in api/observation-verification-2026-10-25.md." }, "spec": "static/openapi/agent-service.json", "securitySchemes": [ @@ -1141,7 +1168,7 @@ "resultDelivery": "Checks trusted user ownership before body/image reads. Missing, cleared and unowned Runs return 404. Delegated Runs return 403 delegated_run_requires_inspect_view; use the messages inspection view instead. pendingAssistant is temporary and separate from durable messages. Returned configuration is not execution authority. Sensitive tool results may be withheld in the display projection; body references can be removed without changing the underlying execution outcome.", "source": { "repository": "desirecore/desirecore", - "revision": "a101a40c14cc9d478c7f9a23d8a125ac2404509b", + "revision": "a0186f6feeaf66eb88787e8b80b8fea5559b8761", "files": [ "packages/agent-service/src/routes/history-routes.ts", "packages/agent-service/src/session/store.ts", @@ -1160,7 +1187,8 @@ "verification": { "record": "api/stability-verification-2026-10-22.md", "scope": "Fixed-source route/owner/pagination review; real local source history routes and SessionStore fixture verifies authentication, owner isolation, public projection, search validation and empty/404 matter reads. The independent example uses 13 local HTTP contract tests, including paging gaps and truncation; no deployed-instance claim.", - "baselineNote": "Source contract reviewed at a101a40c on 2026-10-22. Historical test results retain their own recorded baseline; current validation is recorded in api/stability-verification-2026-10-22.md." + "baselineNote": "Source contract reviewed at a101a40c on 2026-10-22. Historical test results retain their own recorded baseline; current validation is recorded in api/stability-verification-2026-10-22.md.", + "currentReview": "Static contract recheck at a0186f6feeaf66eb88787e8b80b8fea5559b8761 on 2026-10-25; historical executed tests retain their recorded baseline. Current observation checks are in api/observation-verification-2026-10-25.md." }, "spec": "static/openapi/agent-service.json", "securitySchemes": [ @@ -1181,7 +1209,7 @@ "resultDelivery": "Checks ownership before content reads. Delegated Runs require view=inspect-run. total counts source rows, not returned messages; advance offset by the requested limit and stop at total, even if a page is empty. Optional live is a temporary delegated display checkpoint, not a durable message or terminal. There is no cross-page consistency revision. Sensitive tool results may be withheld in the display projection; body references can be removed without changing the underlying execution outcome.", "source": { "repository": "desirecore/desirecore", - "revision": "a101a40c14cc9d478c7f9a23d8a125ac2404509b", + "revision": "a0186f6feeaf66eb88787e8b80b8fea5559b8761", "files": [ "packages/agent-service/src/routes/history-routes.ts", "packages/agent-service/src/session/store.ts", @@ -1200,7 +1228,8 @@ "verification": { "record": "api/stability-verification-2026-10-22.md", "scope": "Fixed-source route/owner/pagination review; real local source history routes and SessionStore fixture verifies authentication, owner isolation, public projection, search validation and empty/404 matter reads. The independent example uses 13 local HTTP contract tests, including paging gaps and truncation; no deployed-instance claim.", - "baselineNote": "Source contract reviewed at a101a40c on 2026-10-22. Historical test results retain their own recorded baseline; current validation is recorded in api/stability-verification-2026-10-22.md." + "baselineNote": "Source contract reviewed at a101a40c on 2026-10-22. Historical test results retain their own recorded baseline; current validation is recorded in api/stability-verification-2026-10-22.md.", + "currentReview": "Static contract recheck at a0186f6feeaf66eb88787e8b80b8fea5559b8761 on 2026-10-25; historical executed tests retain their recorded baseline. Current observation checks are in api/observation-verification-2026-10-25.md." }, "spec": "static/openapi/agent-service.json", "securitySchemes": [ @@ -1221,7 +1250,7 @@ "resultDelivery": "Matches extracted message text case-insensitively, newest timestamp first. Includes independent manual conversations; excludes archived and delegated Runs. Default-conversation fork families select the visible history window. preferredRunId is neither a credential nor an exact-Run filter. limit/offset and their sum must be safe nonnegative integers. limit=0 returns all remaining hits and hasMore=false. Match original text before cropping previews.", "source": { "repository": "desirecore/desirecore", - "revision": "a101a40c14cc9d478c7f9a23d8a125ac2404509b", + "revision": "a0186f6feeaf66eb88787e8b80b8fea5559b8761", "files": [ "packages/agent-service/src/routes/history-routes.ts", "packages/agent-service/src/session/store.ts", @@ -1238,7 +1267,8 @@ "verification": { "record": "api/history-attachments-verification.md", "scope": "Fixed-source route/owner/pagination review; real local source history routes and SessionStore fixture verifies authentication, owner isolation, public projection, search validation and empty/404 matter reads. The independent example uses 13 local HTTP contract tests, including paging gaps and truncation; no deployed-instance claim.", - "baselineNote": "Source contract reviewed at a101a40c on 2026-10-22. Historical test results retain their own recorded baseline; current validation is recorded in api/stability-verification-2026-10-22.md." + "baselineNote": "Source contract reviewed at a101a40c on 2026-10-22. Historical test results retain their own recorded baseline; current validation is recorded in api/stability-verification-2026-10-22.md.", + "currentReview": "Static contract recheck at a0186f6feeaf66eb88787e8b80b8fea5559b8761 on 2026-10-25; historical executed tests retain their recorded baseline. Current observation checks are in api/observation-verification-2026-10-25.md." }, "spec": "static/openapi/agent-service.json", "securitySchemes": [ @@ -1259,7 +1289,7 @@ "resultDelivery": "Reads the instance's current local profile, not a caller-supplied tenant ID. Intended for the deployment owner. Returns all active matters and at most ten recent entries, not a complete transcript. No pagination. An empty overview does not establish Agent existence or absence of history.", "source": { "repository": "desirecore/desirecore", - "revision": "a101a40c14cc9d478c7f9a23d8a125ac2404509b", + "revision": "a0186f6feeaf66eb88787e8b80b8fea5559b8761", "files": [ "packages/agent-service/src/routes/conversation-routes.ts", "packages/agent-service/src/conversation-memory/overview-projection.ts", @@ -1275,7 +1305,8 @@ "verification": { "record": "api/history-attachments-verification.md", "scope": "Fixed-source route/owner/pagination review; real local source history routes and SessionStore fixture verifies authentication, owner isolation, public projection, search validation and empty/404 matter reads. The independent example uses 13 local HTTP contract tests, including paging gaps and truncation; no deployed-instance claim.", - "baselineNote": "Source contract reviewed at a101a40c on 2026-10-22. Historical test results retain their own recorded baseline; current validation is recorded in api/stability-verification-2026-10-22.md." + "baselineNote": "Source contract reviewed at a101a40c on 2026-10-22. Historical test results retain their own recorded baseline; current validation is recorded in api/stability-verification-2026-10-22.md.", + "currentReview": "Static contract recheck at a0186f6feeaf66eb88787e8b80b8fea5559b8761 on 2026-10-25; historical executed tests retain their recorded baseline. Current observation checks are in api/observation-verification-2026-10-25.md." }, "spec": "static/openapi/agent-service.json", "securitySchemes": [ @@ -1296,7 +1327,7 @@ "resultDelivery": "Reads the instance owner's local-profile conversation memory. Supports active, recent and archived source data. Underlying Run reads recheck that user and Agent ownership. Reads at most 120 source messages and sets truncated=true if more exist; projection may omit entries. No pagination or limit-override parameter. Entries are not guaranteed to have the per-Run message shape. Sensitive tool results may be withheld in the display projection; body references can be removed without changing the underlying execution outcome.", "source": { "repository": "desirecore/desirecore", - "revision": "a101a40c14cc9d478c7f9a23d8a125ac2404509b", + "revision": "a0186f6feeaf66eb88787e8b80b8fea5559b8761", "files": [ "packages/agent-service/src/routes/conversation-routes.ts", "packages/agent-service/src/conversation-memory/conversation-store.ts", @@ -1314,7 +1345,196 @@ "verification": { "record": "api/stability-verification-2026-10-22.md", "scope": "Fixed-source route/owner/pagination review; real local source history routes and SessionStore fixture verifies authentication, owner isolation, public projection, search validation and empty/404 matter reads. The independent example uses 13 local HTTP contract tests, including paging gaps and truncation; no deployed-instance claim.", - "baselineNote": "Source contract reviewed at a101a40c on 2026-10-22. Historical test results retain their own recorded baseline; current validation is recorded in api/stability-verification-2026-10-22.md." + "baselineNote": "Source contract reviewed at a101a40c on 2026-10-22. Historical test results retain their own recorded baseline; current validation is recorded in api/stability-verification-2026-10-22.md.", + "currentReview": "Static contract recheck at a0186f6feeaf66eb88787e8b80b8fea5559b8761 on 2026-10-25; historical executed tests retain their recorded baseline. Current observation checks are in api/observation-verification-2026-10-25.md." + }, + "spec": "static/openapi/agent-service.json", + "securitySchemes": [ + "InstanceToken" + ] + }, + { + "operationId": "getAgentInputConfig", + "service": "agent-service", + "method": "GET", + "path": "/api/agents/:id/input-config", + "doc": "api/conversations-and-runs", + "deploymentModes": [ + "standalone-owner" + ], + "authentication": "Existing instance-owner X-Agent-Service-Token.", + "permissions": "Fresh input configuration only; does not create a session, lock mode, or grant execution permission. configRevision is an equality digest.", + "resultDelivery": "Synchronous JSON observation; no execution request or automatic replay.", + "source": { + "repository": "desirecore/desirecore", + "revision": "a0186f6feeaf66eb88787e8b80b8fea5559b8761", + "files": [ + "packages/agent-service/src/routes/agent-input-config-route.ts", + "packages/agent-service/src/routes/agent-input-config-observation.ts", + "packages/agent-service/src/agent/input-config.ts", + "packages/agent-service/src/routes/agent-routes.ts", + "packages/agent-service/src/auth/instance-auth.ts", + "deploy/Caddyfile" + ] + }, + "schemaRefs": [ + "packages/schemas/src/agent-service/input-config.ts:agentInputConfigSchema" + ], + "effectiveVersion": "v10.0.180+", + "versionEvidence": "Input-config introduction: implementation PR #4158 and its client v10.0.180 support statement, preserved from the internal Agent API reference.", + "verification": { + "record": "api/observation-verification-2026-10-25.md", + "scope": "See the fixed-source route fixtures, index migration regressions and independent client checks; no model/provider or production execution claim." + }, + "spec": "static/openapi/agent-service.json", + "securitySchemes": [ + "InstanceToken" + ] + }, + { + "operationId": "listMergedHistoryMessages", + "service": "agent-service", + "method": "GET", + "path": "/api/sessions/messages/all", + "doc": "api/session-history-and-messages", + "deploymentModes": [ + "standalone-owner" + ], + "authentication": "Existing instance-owner X-Agent-Service-Token.", + "permissions": "Trusted-user visible merged history; apply public message projection and branch/manual/Mention rules before pagination and revision. Synthetic source-only run IDs are not execution identities.", + "resultDelivery": "Synchronous JSON observation; no execution request or automatic replay.", + "source": { + "repository": "desirecore/desirecore", + "revision": "a0186f6feeaf66eb88787e8b80b8fea5559b8761", + "files": [ + "packages/agent-service/src/routes/history-routes.ts", + "packages/agent-service/src/session/store.ts", + "packages/agent-service/src/session/public-session-projection.ts", + "packages/agent-service/src/session/conversation-mention-history.ts", + "packages/agent-service/src/auth/instance-auth.ts", + "deploy/Caddyfile" + ] + }, + "schemaRefs": [ + "packages/schemas/src/agent-service/session.ts:sessionMessageSchema" + ], + "effectiveVersion": null, + "versionEvidence": null, + "verification": { + "record": "api/observation-verification-2026-10-25.md", + "scope": "See the fixed-source route fixtures, index migration regressions and independent client checks; no model/provider or production execution claim." + }, + "spec": "static/openapi/agent-service.json", + "securitySchemes": [ + "InstanceToken" + ] + }, + { + "operationId": "getNetworkHealth", + "service": "agent-service", + "method": "GET", + "path": "/api/network/health", + "doc": "api/observability", + "deploymentModes": [ + "standalone-owner" + ], + "authentication": "Existing instance-owner X-Agent-Service-Token.", + "permissions": "Instance network observation; does not provide per-tenant isolation, execution authority or model success evidence.", + "resultDelivery": "Synchronous JSON observation; no execution request or automatic replay.", + "source": { + "repository": "desirecore/desirecore", + "revision": "a0186f6feeaf66eb88787e8b80b8fea5559b8761", + "files": [ + "packages/agent-service/src/routes/network-routes.ts", + "packages/agent-service/src/connection/network-health.ts", + "packages/agent-service/src/handlers/network-handlers.ts", + "packages/agent-service/src/auth/instance-auth.ts", + "deploy/Caddyfile" + ] + }, + "schemaRefs": [ + "packages/schemas/src/agent-service/network-health.ts:networkHealthStateSchema" + ], + "effectiveVersion": null, + "versionEvidence": null, + "verification": { + "record": "api/observation-verification-2026-10-25.md", + "scope": "See the fixed-source route fixtures, index migration regressions and independent client checks; no model/provider or production execution claim." + }, + "spec": "static/openapi/agent-service.json", + "securitySchemes": [ + "InstanceToken" + ] + }, + { + "operationId": "listRecentToolCalls", + "service": "agent-service", + "method": "GET", + "path": "/api/runs/recent-calls", + "doc": "api/observability", + "deploymentModes": [ + "standalone-owner" + ], + "authentication": "Existing instance-owner X-Agent-Service-Token.", + "permissions": "Trusted-user Run filter; metadata only, bounded recent window. Opaque IDs, status and timestamps are observations rather than query terminal evidence.", + "resultDelivery": "Synchronous JSON observation; no execution request or automatic replay.", + "source": { + "repository": "desirecore/desirecore", + "revision": "a0186f6feeaf66eb88787e8b80b8fea5559b8761", + "files": [ + "packages/agent-service/src/routes/runs-routes.ts", + "packages/agent-service/src/session/runs-index.ts", + "packages/agent-service/src/receipts/recent-tool-calls.ts", + "packages/agent-service/src/receipts/identity.ts", + "packages/agent-service/src/receipts/reader.ts", + "packages/agent-service/src/auth/instance-auth.ts", + "deploy/Caddyfile" + ] + }, + "schemaRefs": [ + "packages/schemas/src/agent-service/operations-activity.ts:operationsRecentCallsResponseSchema" + ], + "effectiveVersion": null, + "versionEvidence": null, + "verification": { + "record": "api/observation-verification-2026-10-25.md", + "scope": "See the fixed-source route fixtures, index migration regressions and independent client checks; no model/provider or production execution claim." + }, + "spec": "static/openapi/agent-service.json", + "securitySchemes": [ + "InstanceToken" + ] + }, + { + "operationId": "getRunUsageSummary", + "service": "agent-service", + "method": "GET", + "path": "/api/runs/usage/summary", + "doc": "api/observability", + "deploymentModes": [ + "standalone-owner" + ], + "authentication": "Existing instance-owner X-Agent-Service-Token.", + "permissions": "Trusted-user recorded usage only; limited scans/cache and UTC date bucket remapping. Not a final provider invoice or exact local day accounting.", + "resultDelivery": "Synchronous JSON observation; no execution request or automatic replay.", + "source": { + "repository": "desirecore/desirecore", + "revision": "a0186f6feeaf66eb88787e8b80b8fea5559b8761", + "files": [ + "packages/agent-service/src/routes/runs-routes.ts", + "packages/agent-service/src/session/runs-index.ts", + "packages/agent-service/src/auth/instance-auth.ts", + "deploy/Caddyfile" + ] + }, + "schemaRefs": [ + "packages/schemas/src/agent-service/operations-activity.ts:operationsUsageSummaryResponseSchema" + ], + "effectiveVersion": null, + "versionEvidence": null, + "verification": { + "record": "api/observation-verification-2026-10-25.md", + "scope": "See the fixed-source route fixtures, index migration regressions and independent client checks; no model/provider or production execution claim." }, "spec": "static/openapi/agent-service.json", "securitySchemes": [ diff --git a/api/examples/agent-client/README.md b/api/examples/agent-client/README.md index 01ed5b0..463f662 100644 --- a/api/examples/agent-client/README.md +++ b/api/examples/agent-client/README.md @@ -76,7 +76,7 @@ Run `npm test` for the independent HTTP + Socket.IO contract fixture. It verifie `test/source-route.fixture.ts` can be copied into a fixed-source DesireCore checkout as `packages/agent-service/src/__tests__/api-docs-node-client.integration.test.ts`. Set `DESIRECORE_API_EXAMPLE_PATH` to the absolute path of this directory's `client.mjs`, then run that single file in the repository's integration test project. It verifies this client against the actual query routes, HTTP/Socket authentication, request Schema and digest, durable admission registry and lookup, and renderer delivery room. The query runtime and follow-up execution callback remain fake. Use its isolated temporary data root; do not point it at an existing user profile. -Current source review: `desirecore/desirecore` commit `a101a40c14cc9d478c7f9a23d8a125ac2404509b`. Historical validation remains in `api/verification.md`; this extension is recorded in `api/history-attachments-verification.md`. +Current source review: `desirecore/desirecore` commit `a0186f6feeaf66eb88787e8b80b8fea5559b8761` (fixed review snapshot; source PR #5194). Historical validation remains in `api/verification.md`; this extension is recorded in `api/history-attachments-verification.md`. - HTTP submission: `packages/agent-service/src/routes/query-routes.ts`. - Admission lookup: `packages/agent-service/src/routes/client-request-lookup-routes.ts`; shared Schema in `packages/schemas/src/agent-service/client-request-admission.ts`. @@ -90,3 +90,18 @@ Keep the three published downloads byte-identical to their maintained files: `cl `test/source-history.fixture.ts` can be copied to the fixed source tree as `packages/agent-service/src/__tests__/api-docs-history.test.ts` and run in the `unit` project. It uses actual instance authentication, history/topic/Run routes and temporary file storage. The local profile and runtime discovery are stubs. It checks owner filtering, public message projection, search validation, empty topic reads, and the actual `handed_off` detail/`404` runtime-state behavior. It does not run a model or real handoff. Website TypeScript checking excludes these two source fixtures because their imports resolve only inside DesireCore’s source repository. History output is a display projection: sensitive tool results may be placeholders, and body references may be absent. The reader preserves these payloads and optional response extensions; it does not infer tool success from placeholder text, retrieve withheld references, or execute replacement work. Current stability review and verification are recorded in `api/stability-verification-2026-10-22.md`. + + +## Observation reader + +`observe.mjs` requires Node.js 22+ and no npm packages. It uses the same administrator-provided instance origin and owner credential, checks the handshake, performs one GET, and preserves the returned data. It does not poll, replay work or reconcile billing. The example validates recommended input formats; service fallback behavior is documented separately. + +```bash +node observe.mjs input-config --agent-id your-agent-id +node observe.mjs network +node observe.mjs recent-calls --limit 20 +node observe.mjs usage --since 2026-10-01T00:00:00Z --timezone UTC +node history.mjs all --agent-id your-agent-id --limit 100 --all-pages +``` + +Merged history buffers one revision per attempt. A 409 discards that attempt and restarts from page zero within the configured restart budget. No rejected attempt is emitted as a successful page set. Source-only Mention IDs remain display identifiers and are never passed to Run or execution APIs. `limit=0` returns metadata with an empty page rather than all messages. Current validation is recorded in `api/observation-verification-2026-10-25.md`. diff --git a/api/examples/agent-client/history.mjs b/api/examples/agent-client/history.mjs index b1ce2ad..8796cf3 100644 --- a/api/examples/agent-client/history.mjs +++ b/api/examples/agent-client/history.mjs @@ -7,27 +7,34 @@ const HELP = `Usage: node history.mjs list [--agent-id ID] [--include-archived] [--all-pages] node history.mjs session RUN_ID node history.mjs messages RUN_ID [--inspect-run] [--all-pages] + node history.mjs all --agent-id ID [--conversation-id ID] [--order asc|desc] [--all-pages] node history.mjs search --agent-id ID [--query TEXT] [--type all|image|file|tool] [--all-pages] node history.mjs overview --agent-id ID node history.mjs matter MATTER_ID --agent-id ID Environment: DESIRECORE_URL, DESIRECORE_TOKEN, optional DESIRECORE_AGENT_ID. -Pagination: --offset 0; --limit defaults to list=20, messages=100, search=50. -Search alone accepts --limit 0 (all remaining hits), --preferred-run-id ID, -and --preview-chars 1..8000. --all-pages is bounded by --max-pages 20. +Pagination: --offset 0; --limit defaults to list=20, messages/all=100, search=50. +all accepts --limit 0 (metadata only), --preferred-run-id ID, --expected-revision h1:HASH, +and --max-restarts 1. A later offset requires --expected-revision. Revision conflicts +discard buffered pages and restart all-pages reads at 0; no execution is replayed. +Search accepts --limit 0 (all remaining hits), --preferred-run-id ID, and +--preview-chars 1..8000. --all-pages is bounded by --max-pages 20 per attempt. HTTP: --timeout-ms 10000. Remote origins must use HTTPS. Output: one JSON record per page; history may contain private conversation data. Exit: 0 requested reads succeeded; 1 request/configuration failed; 2 an all-pages read stopped early or a matter response was truncated. -These endpoints do not provide a consistent multi-page history snapshot. +Only all checks the public view revision; it is not a persistent snapshot or backup. `; -const PAGED = new Set(['list', 'messages', 'search']); -const NUMERIC = new Set(['limit', 'offset', 'maxPages', 'timeoutMs', 'previewChars']); +const PAGED = new Set(['list', 'messages', 'search', 'all']); +const NUMERIC = new Set(['limit', 'offset', 'maxPages', 'maxRestarts', 'timeoutMs', 'previewChars']); +const REVISION = /^h1:[0-9a-f]{64}$/; const VALUES = new Map([ ['--agent-id', 'agentId'], ['--limit', 'limit'], ['--offset', 'offset'], ['--query', 'query'], ['--type', 'type'], ['--preferred-run-id', 'preferredRunId'], ['--preview-chars', 'previewChars'], ['--max-pages', 'maxPages'], ['--timeout-ms', 'timeoutMs'], + ['--conversation-id', 'conversationId'], ['--order', 'order'], + ['--expected-revision', 'expectedRevision'], ['--max-restarts', 'maxRestarts'], ]); const FLAGS = new Map([ ['--all-pages', 'allPages'], ['--include-archived', 'includeArchived'], ['--inspect-run', 'inspectRun'], @@ -68,10 +75,10 @@ function identifier(value, label) { function optionsFor(input, env) { const options = { ...input }; - if (!['list', 'session', 'messages', 'search', 'overview', 'matter'].includes(options.command)) throw new Error(HELP); + if (!['list', 'session', 'messages', 'all', 'search', 'overview', 'matter'].includes(options.command)) throw new Error(HELP); options.agentId ??= env.DESIRECORE_AGENT_ID; if (options.agentId !== undefined) identifier(options.agentId, 'Agent ID'); - if (['search', 'overview', 'matter'].includes(options.command) && !options.agentId) throw new Error('Set DESIRECORE_AGENT_ID or --agent-id.'); + if (['all', 'search', 'overview', 'matter'].includes(options.command) && !options.agentId) throw new Error('Set DESIRECORE_AGENT_ID or --agent-id.'); if (['session', 'messages'].includes(options.command)) identifier(options.id, 'Run ID'); else if (options.command === 'matter') { if (typeof options.id !== 'string' || !options.id || /[\\/\u0000-\u001f\u007f]/.test(options.id) || options.id.includes('..') || options.id === '.') { @@ -85,11 +92,19 @@ function optionsFor(input, env) { if (!Number.isSafeInteger(options[key]) || options[key] < 0) throw new Error(`Invalid ${key}.`); } } - options.limit ??= options.command === 'list' ? 20 : options.command === 'messages' ? 100 : 50; + options.limit ??= options.command === 'list' ? 20 : ['messages', 'all'].includes(options.command) ? 100 : 50; options.offset ??= 0; options.maxPages ??= 20; + options.maxRestarts ??= 1; options.timeoutMs ??= 10_000; - if ((options.limit === 0 && options.command !== 'search') || !Number.isSafeInteger(options.offset + options.limit)) throw new Error('Use a positive limit and a safe offset; only search permits limit=0.'); + if ((options.limit === 0 && !['search', 'all'].includes(options.command)) || !Number.isSafeInteger(options.offset + options.limit)) throw new Error('Use a positive limit and a safe offset; only search and all permit limit=0.'); + if (options.command === 'all') { + if (options.limit > 1000) throw new Error('all limit must be 0..1000.'); + if (options.expectedRevision !== undefined && !REVISION.test(options.expectedRevision)) throw new Error('Use the returned h1: revision as --expected-revision.'); + if (options.offset > 0 && options.expectedRevision === undefined) throw new Error('A later all page requires --expected-revision from the same read scope.'); + if (options.order !== undefined && !['asc', 'desc'].includes(options.order)) throw new Error('order must be asc or desc.'); + if (options.conversationId !== undefined && (typeof options.conversationId !== 'string' || !options.conversationId || /[\u0000-\u001f\u007f]/.test(options.conversationId))) throw new Error('Use a nonempty conversation ID.'); + } else if (['conversationId', 'order', 'expectedRevision', 'maxRestarts'].some(key => input[key] !== undefined)) throw new Error('Revision and merged-history options apply only to all.'); if (options.maxPages < 1 || options.timeoutMs < 1) throw new Error('maxPages and timeoutMs must be positive.'); if (options.previewChars !== undefined && (options.previewChars < 1 || options.previewChars > 8000)) throw new Error('previewChars must be 1..8000.'); if (options.type !== undefined && !['all', 'image', 'file', 'tool'].includes(options.type)) throw new Error('type must be all, image, file or tool.'); @@ -97,17 +112,24 @@ function optionsFor(input, env) { if (options.includeArchived && options.command !== 'list') throw new Error('--include-archived applies only to list.'); if (options.inspectRun && options.command !== 'messages') throw new Error('--inspect-run applies only to messages.'); if (!PAGED.has(options.command) && (options.allPages || input.limit !== undefined || input.offset !== undefined)) throw new Error('This endpoint is not paginated.'); - if (options.command !== 'search' && ['query', 'type', 'preferredRunId', 'previewChars'].some(key => options[key] !== undefined)) throw new Error('Search filters apply only to search.'); + if (options.command !== 'search' && ['query', 'type', 'previewChars'].some(key => options[key] !== undefined)) throw new Error('Search filters apply only to search.'); + if (!['search', 'all'].includes(options.command) && options.preferredRunId !== undefined) throw new Error('--preferred-run-id applies only to search or all.'); return options; } -function endpoint(options, offset) { +function endpoint(options, offset, expectedRevision) { const query = new URLSearchParams(); let path; if (options.command === 'list') { path = '/api/sessions'; if (options.agentId) query.set('agentId', options.agentId); if (options.includeArchived) query.set('includeArchived', 'true'); + } else if (options.command === 'all') { + path = '/api/sessions/messages/all'; + query.set('agentId', options.agentId); + for (const [key, value] of Object.entries({ conversationId: options.conversationId, order: options.order, preferredRunId: options.preferredRunId, expectedRevision })) { + if (value !== undefined) query.set(key, value); + } } else if (options.command === 'search') { path = '/api/sessions/messages/search'; query.set('agentId', options.agentId); @@ -145,6 +167,7 @@ export async function runHistory(input, env = process.env, output = {}) { const error = new Error(`HTTP ${response.status}: ${typeof data?.error === 'string' ? data.error : 'request rejected'}`); error.status = response.status; error.code = typeof data?.code === 'string' ? data.code : undefined; + error.apiError = typeof data?.error === 'string' ? data.error : undefined; throw error; } return data; @@ -153,6 +176,42 @@ export async function runHistory(input, env = process.env, output = {}) { if (handshake.service !== 'desirecore-agent-service' || handshake.apiProtocolVersion !== '1' || typeof handshake.instanceId !== 'string' || !handshake.instanceId) { throw new Error('Unexpected service identity or API protocol; history was not requested.'); } + if (options.command === 'all') { + let offset = options.offset, revision = options.expectedRevision, restarts = 0, buffered = []; + while (true) { + let data; + try { data = await get(endpoint(options, offset, revision)); } + catch (error) { + if (error.status !== 409 || error.apiError !== 'history_revision_changed' || !options.allPages) throw error; + // Nothing from an invalidated attempt has been emitted. Restart only GET reads. + buffered = []; + if (restarts >= options.maxRestarts) { + emit({ kind: 'stopped', reason: 'history-revision-changed', restartOffset: 0, discarded: true }); + return { exitCode: 2, pages: 0, restarts, restartOffset: 0 }; + } + emit({ kind: 'restarting', reason: 'history-revision-changed', restartOffset: 0, discarded: true }); + restarts++; offset = 0; revision = undefined; + continue; + } + if (!Array.isArray(data?.items) || data.items.length > options.limit || !Number.isSafeInteger(data.total) || data.total < 0 + || typeof data.hasMore !== 'boolean' || !REVISION.test(data.historyRevision) + || (revision !== undefined && revision !== data.historyRevision)) throw new Error('Unexpected merged history page or revision.'); + revision = data.historyRevision; + buffered.push({ kind: 'all', instanceId: handshake.instanceId, offset, limit: options.limit, data }); + const nextOffset = offset + options.limit; + if (!data.hasMore || !options.allPages || options.limit === 0) { + for (const record of buffered) emit(record); + return { exitCode: 0, pages: buffered.length, restarts, ...(data.hasMore && options.limit > 0 && { nextOffset, historyRevision: revision }) }; + } + if (buffered.length >= options.maxPages || !Number.isSafeInteger(nextOffset)) { + // A bounded partial window is explicitly marked and retains its revision for a later read. + for (const record of buffered) emit(record); + emit({ kind: 'stopped', reason: buffered.length >= options.maxPages ? 'max-pages' : 'invalid-next-offset', nextOffset, historyRevision: revision }); + return { exitCode: 2, pages: buffered.length, restarts, nextOffset, historyRevision: revision }; + } + offset = nextOffset; + } + } let offset = options.offset, pages = 0, firstTotal; while (true) { const data = await get(endpoint(options, offset)); diff --git a/api/examples/agent-client/observe.mjs b/api/examples/agent-client/observe.mjs new file mode 100644 index 0000000..3630809 --- /dev/null +++ b/api/examples/agent-client/observe.mjs @@ -0,0 +1,95 @@ +#!/usr/bin/env node +// Standalone observation reads. Node.js 22+, no packages required. +import { resolve } from 'node:path'; +import { pathToFileURL } from 'node:url'; + +const HELP = `Usage: + node observe.mjs input-config --agent-id ID + node observe.mjs network + node observe.mjs recent-calls [--limit 30] + node observe.mjs usage [--since ISO_DATE] [--agent-id ID] [--timezone UTC] +Environment: DESIRECORE_URL, DESIRECORE_TOKEN, optional DESIRECORE_AGENT_ID. +Options: --timeout-ms 10000. Remote origins must use HTTPS. +Output contains private instance metadata. No automatic retry, execution, or billing reconciliation. +`; + +function originFor(value) { + let url; + try { url = new URL(value); } catch { throw new Error('Set DESIRECORE_URL to the instance origin.'); } + if (url.username || url.password || url.pathname !== '/' || url.search || url.hash + || (url.protocol !== 'https:' && !(url.protocol === 'http:' && ['localhost', '127.0.0.1', '[::1]'].includes(url.hostname)))) { + throw new Error('Use an HTTPS origin without credentials, path, query or fragment; HTTP is allowed only on loopback.'); + } + return url.origin; +} + +export function parseObserveArgs(argv) { + if (argv.includes('--help') || argv.includes('-h')) return { help: true }; + const options = { command: argv[0] }; + const flags = { '--agent-id': 'agentId', '--limit': 'limit', '--since': 'since', '--timezone': 'timezone', '--timeout-ms': 'timeoutMs' }; + for (let i = 1; i < argv.length; i++) { + const key = flags[argv[i]]; + if (!key || Object.hasOwn(options, key) || !argv[i + 1] || argv[i + 1].startsWith('--')) throw new Error(HELP); + options[key] = argv[++i]; + } + return options; +} + +export async function runObserve(input, { env = process.env, emit = value => process.stdout.write(`${JSON.stringify(value)}\n`) } = {}) { + if (input.help) { process.stdout.write(HELP); return; } + const { command } = input; + if (!['input-config', 'network', 'recent-calls', 'usage'].includes(command)) throw new Error(HELP); + const origin = originFor(env.DESIRECORE_URL); + if (typeof env.DESIRECORE_TOKEN !== 'string' || !env.DESIRECORE_TOKEN.trim()) throw new Error('Set DESIRECORE_TOKEN.'); + const allowed = new Set(['command', 'help', 'timeoutMs', ...(command === 'input-config' ? ['agentId'] : command === 'usage' ? ['agentId', 'since', 'timezone'] : command === 'recent-calls' ? ['limit'] : [])]); + if (Object.keys(input).some(key => !allowed.has(key))) throw new Error('An option does not apply to this command.'); + const agentId = ['input-config', 'usage'].includes(command) ? input.agentId ?? env.DESIRECORE_AGENT_ID : undefined; + if ((command === 'input-config' && !agentId) || (agentId !== undefined && !/^[A-Za-z0-9_-]+$/.test(agentId))) throw new Error('Use the Agent ID returned by this instance.'); + const integer = (value, label, fallback) => { + if (value === undefined) return fallback; + if (!/^[1-9][0-9]*$/.test(String(value)) || !Number.isSafeInteger(Number(value))) throw new Error(`${label} must be a positive decimal safe integer.`); + return Number(value); + }; + const timeoutMs = integer(input.timeoutMs, 'timeoutMs', 10_000); + const limit = integer(input.limit, 'limit', 30); + if (input.since !== undefined && !/^\d{4}-\d{2}-\d{2}T.*(?:Z|[+-]\d{2}:\d{2})$/.test(input.since)) throw new Error('Use an ISO timestamp with timezone for --since.'); + if (input.since !== undefined && !Number.isFinite(Date.parse(input.since))) throw new Error('Invalid --since timestamp.'); + if (input.timezone !== undefined) { + try { new Intl.DateTimeFormat('en', { timeZone: input.timezone }); } catch { throw new Error('Use a valid IANA --timezone, for example UTC.'); } + } + async function get(path) { + const response = await fetch(origin + path, { + headers: { 'X-Agent-Service-Token': env.DESIRECORE_TOKEN }, + redirect: 'error', signal: AbortSignal.timeout(timeoutMs), + }); + if (!response.headers.get('content-type')?.toLowerCase().includes('application/json')) throw new Error(`HTTP ${response.status}: expected JSON from the instance API.`); + const value = await response.json(); + if (!response.ok) { + const code = typeof value?.code === 'string' && /^[A-Za-z0-9_-]{1,100}$/.test(value.code) ? ` ${value.code}` : ''; + throw new Error(`Observation read rejected: HTTP ${response.status}${code}`); + } + if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error('Expected an API response object.'); + return value; + } + const metadata = await get('/api/client-handshake'); + if (metadata.service !== 'desirecore-agent-service' || metadata.apiProtocolVersion !== '1' || typeof metadata.instanceId !== 'string' || !metadata.instanceId) throw new Error('The handshake did not identify a compatible DesireCore instance.'); + const query = new URLSearchParams(); + let path; + if (command === 'input-config') path = `/api/agents/${encodeURIComponent(agentId)}/input-config`; + else if (command === 'network') path = '/api/network/health'; + else if (command === 'recent-calls') { path = '/api/runs/recent-calls'; query.set('limit', String(limit)); } + else { + path = '/api/runs/usage/summary'; + for (const [key, value] of Object.entries({ since: input.since, agentId, timezone: input.timezone })) if (value !== undefined) query.set(key, value); + } + const data = await get(path + (query.size ? `?${query}` : '')); + if (command === 'input-config' && (data.version !== 1 || data.agentId !== agentId || typeof data.manual !== 'boolean' || !/^[0-9a-f]{64}$/.test(data.configRevision))) throw new Error('Invalid input configuration; do not assume manual=false.'); + if (command === 'network' && (!['online', 'degraded', 'offline', 'unknown'].includes(data.status) || !Number.isFinite(data.seq) || data.seq < 0 || typeof data.since !== 'string' || typeof data.reason !== 'string')) throw new Error('Invalid network observation.'); + if (command === 'recent-calls' && !Array.isArray(data.calls)) throw new Error('Invalid recent calls observation.'); + if (command === 'usage' && (!data.total || ['byModel', 'byDate', 'byAgent'].some(key => !Array.isArray(data[key])))) throw new Error('Invalid usage observation.'); + emit({ kind: command, instanceId: metadata.instanceId, data }); +} + +if (process.argv[1] && import.meta.url === pathToFileURL(resolve(process.argv[1])).href) { + runObserve(parseObserveArgs(process.argv.slice(2))).catch(error => { process.stderr.write(`${error.message}\n`); process.exitCode = 1; }); +} diff --git a/api/examples/agent-client/test/history.test.mjs b/api/examples/agent-client/test/history.test.mjs index 7190ac5..7fc99f1 100644 --- a/api/examples/agent-client/test/history.test.mjs +++ b/api/examples/agent-client/test/history.test.mjs @@ -180,3 +180,112 @@ test('validates flags, IDs, finite pagination and HTTPS before network access', } await assert.rejects(runHistory({ command: 'list' }, { DESIRECORE_URL: 'http://remote.example', DESIRECORE_TOKEN: token }), /Use HTTPS/); }); + +test('merged history preserves scope and pins later pages to the first public revision', async t => { + const revision = `h1:${'a'.repeat(64)}`; + const source = { runId: '__mention_source:source-1', messageIndex: 0, sessionTime: '2026-10-25T01:00:00Z', + message: { id: 'source-1', role: 'user', content: 'Source display only', mentionSourceOnly: true } }; + const f = await fixture(t, (url, response) => { + assert.equal(url.pathname, '/api/sessions/messages/all'); + assert.equal(url.searchParams.get('agentId'), 'agent-a'); + assert.equal(url.searchParams.get('conversationId'), '__manual:topic a'); + assert.equal(url.searchParams.get('preferredRunId'), 'run-old'); + assert.equal(url.searchParams.get('order'), 'asc'); + const first = url.searchParams.get('offset') === '0'; + assert.equal(url.searchParams.get('expectedRevision'), first ? null : revision); + json(response, 200, { items: first ? [source] : [], total: 2, hasMore: first, historyRevision: revision }); + }); + const out = await read({ command: 'all', agentId: 'agent-a', conversationId: '__manual:topic a', preferredRunId: 'run-old', order: 'asc', limit: 1, allPages: true }, f.env); + assert.equal(out.result.exitCode, 0); + assert.equal(out.result.pages, 2); + assert.deepEqual(out.records[0].data.items, [source]); + assert.deepEqual(f.requests.slice(1).map(item => item.url.searchParams.get('offset')), ['0', '1']); + assert.ok(f.requests.every(item => item.method === 'GET')); + assert.ok(f.requests.every(item => !item.url.pathname.includes('__mention_source'))); +}); + +test('merged history discards an invalidated attempt and restarts reads without mixing or replay', async t => { + const oldRevision = `h1:${'a'.repeat(64)}`, newRevision = `h1:${'b'.repeat(64)}`; + let requests = 0; + const f = await fixture(t, (url, response) => { + requests++; + const offset = url.searchParams.get('offset'); + if (requests === 1) { + assert.equal(offset, '0'); + return json(response, 200, { items: [{ message: { content: 'old secret display' } }], total: 2, hasMore: true, historyRevision: oldRevision }); + } + if (requests === 2) { + assert.equal(offset, '1'); assert.equal(url.searchParams.get('expectedRevision'), oldRevision); + return json(response, 409, { error: 'history_revision_changed' }); + } + if (requests === 3) { + assert.equal(offset, '0'); assert.equal(url.searchParams.has('expectedRevision'), false); + return json(response, 200, { items: [{ message: { content: 'new display' } }], total: 2, hasMore: true, historyRevision: newRevision }); + } + assert.equal(offset, '1'); assert.equal(url.searchParams.get('expectedRevision'), newRevision); + json(response, 200, { items: [], total: 2, hasMore: false, historyRevision: newRevision }); + }); + const out = await read({ command: 'all', agentId: 'agent-a', limit: 1, allPages: true }, f.env); + assert.equal(out.result.exitCode, 0); + assert.equal(out.result.restarts, 1); + assert.equal(out.records[0].kind, 'restarting'); + assert.equal(out.records.filter(item => item.kind === 'all').length, 2); + assert.ok(out.records.filter(item => item.kind === 'all').every(item => item.data.historyRevision === newRevision)); + assert.ok(!JSON.stringify(out.records).includes('old secret display')); + assert.ok(f.requests.every(item => item.method === 'GET')); + assert.equal(f.requests.length, 5); +}); + +test('merged history conflicts are bounded and do not emit discarded pages', async t => { + const revision = `h1:${'c'.repeat(64)}`; + const f = await fixture(t, (url, response) => url.searchParams.get('offset') === '0' + ? json(response, 200, { items: [{ message: { content: 'discard me' } }], total: 3, hasMore: true, historyRevision: revision }) + : json(response, 409, { error: 'history_revision_changed' })); + const out = await read({ command: 'all', agentId: 'agent-a', limit: 1, allPages: true }, f.env); + assert.deepEqual(out.result, { exitCode: 2, pages: 0, restarts: 1, restartOffset: 0 }); + assert.equal(out.records.at(-1).reason, 'history-revision-changed'); + assert.ok(out.records.every(item => item.kind !== 'all')); + assert.equal(f.requests.length, 5); +}); + +test('merged limit=0 returns metadata once; limit=1000 is valid and bounded', async t => { + const revision = `h1:${'d'.repeat(64)}`; + const f = await fixture(t, (url, response) => json(response, 200, { items: [], total: 42, hasMore: false, historyRevision: revision })); + const out = await read({ command: 'all', agentId: 'agent-a', limit: 0, allPages: true }, f.env); + assert.equal(out.result.pages, 1); + assert.equal(out.result.exitCode, 0); + assert.equal(f.requests.length, 2); + assert.equal(f.requests[1].url.searchParams.get('limit'), '0'); + await read({ command: 'all', agentId: 'agent-a', limit: 1000 }, f.env); + assert.equal(f.requests.at(-1).url.searchParams.get('limit'), '1000'); +}); + +test('merged partial windows include a revision; explicit later reads retain it', async t => { + const revision = `h1:${'e'.repeat(64)}`; + const f = await fixture(t, (_url, response) => json(response, 200, { items: [], total: 3, hasMore: true, historyRevision: revision })); + const out = await read({ command: 'all', agentId: 'agent-a', limit: 1, maxPages: 1, allPages: true }, f.env); + assert.deepEqual(out.result, { exitCode: 2, pages: 1, restarts: 0, nextOffset: 1, historyRevision: revision }); + const later = await read({ command: 'all', agentId: 'agent-a', limit: 1, offset: 1, expectedRevision: revision }, f.env); + assert.equal(later.result.exitCode, 0); + assert.equal(f.requests.at(-1).url.searchParams.get('expectedRevision'), revision); +}); + +test('merged single-page errors are surfaced without retries; no mismatched revision is accepted', async t => { + const revision = `h1:${'f'.repeat(64)}`; + const conflict = await fixture(t, (_url, response) => json(response, 409, { error: 'history_revision_changed' })); + await assert.rejects(runHistory({ command: 'all', agentId: 'agent-a', offset: 1, expectedRevision: revision }, conflict.env), error => error.status === 409); + assert.equal(conflict.requests.length, 2); + const mismatch = await fixture(t, (_url, response) => json(response, 200, { items: [], total: 0, hasMore: false, historyRevision: `h1:${'a'.repeat(64)}` })); + await assert.rejects(runHistory({ command: 'all', agentId: 'agent-a', expectedRevision: revision }, mismatch.env), /Unexpected merged history/); +}); + +test('merged flags validate revision, ranges and command scope before network access', async () => { + assert.deepEqual(parseHistoryArgs(['all', '--conversation-id', '__manual:one', '--expected-revision', `h1:${'a'.repeat(64)}`, '--max-restarts', '2']), + { command: 'all', conversationId: '__manual:one', expectedRevision: `h1:${'a'.repeat(64)}`, maxRestarts: '2' }); + for (const options of [ + { command: 'all', agentId: 'a', limit: 1001 }, { command: 'all', agentId: 'a', offset: 1 }, + { command: 'all', agentId: 'a', expectedRevision: 'not-a-revision' }, { command: 'all', agentId: 'a', order: 'random' }, + { command: 'all', agentId: 'a', maxRestarts: -1 }, { command: 'all', agentId: 'a', conversationId: '' }, + { command: 'search', agentId: 'a', conversationId: 'wrong-command' }, { command: 'messages', id: 'run-a', maxRestarts: 1 }, + ]) await assert.rejects(runHistory(options, {})); +}); diff --git a/api/examples/agent-client/test/observe.test.mjs b/api/examples/agent-client/test/observe.test.mjs new file mode 100644 index 0000000..49e540f --- /dev/null +++ b/api/examples/agent-client/test/observe.test.mjs @@ -0,0 +1,67 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import http from 'node:http'; +import { once } from 'node:events'; +import { runObserve } from '../observe.mjs'; + +const handshake = { service: 'desirecore-agent-service', apiProtocolVersion: '1', instanceId: 'inst_observe_fixture' }; +async function fixture(t, handler, metadata = handshake) { + const requests = []; + const server = http.createServer((req, res) => { + requests.push({ path: req.url, token: req.headers['x-agent-service-token'], method: req.method }); + if (req.url === '/api/client-handshake') { res.setHeader('content-type', 'application/json'); res.end(JSON.stringify(metadata)); } + else handler(req, res); + }); + server.listen(0, '127.0.0.1'); await once(server, 'listening'); + t.after(() => { server.closeAllConnections(); server.close(); }); + return { requests, env: { DESIRECORE_URL: `http://127.0.0.1:${server.address().port}`, DESIRECORE_TOKEN: 'private-test-token' } }; +} +function json(res, value, status = 200) { res.writeHead(status, { 'content-type': 'application/json' }); res.end(JSON.stringify(value)); } + +test('observation reads verify identity and preserve optional response fields without interpreting success', async t => { + const values = { + '/api/agents/agent-a/input-config': { version: 1, agentId: 'agent-a', manual: true, configRevision: 'a'.repeat(64), extension: true }, + '/api/network/health': { status: 'unknown', reason: 'init', since: '2026-10-25T00:00:00.000Z', seq: 0 }, + '/api/runs/recent-calls?limit=2': { calls: [] }, + '/api/runs/usage/summary?since=2026-10-01T00%3A00%3A00Z&agentId=agent-a&timezone=UTC': { total: {}, byModel: [], byDate: [], byAgent: [] }, + }; + const f = await fixture(t, (req, res) => { assert.ok(Object.hasOwn(values, req.url), req.url); json(res, values[req.url]); }); + const out = []; + for (const options of [{ command: 'input-config', agentId: 'agent-a' }, { command: 'network' }, { command: 'recent-calls', limit: '2' }, { command: 'usage', agentId: 'agent-a', since: '2026-10-01T00:00:00Z', timezone: 'UTC' }]) await runObserve(options, { env: f.env, emit: value => out.push(value) }); + assert.equal(out.length, 4); assert.equal(out[0].data.extension, true); assert.equal(out[1].data.status, 'unknown'); + assert.ok(f.requests.every(req => req.method === 'GET' && req.token === f.env.DESIRECORE_TOKEN)); + assert.equal(f.requests.length, 8); +}); + +test('an incorrect handshake prevents reading a different service', async t => { + const f = await fixture(t, () => assert.fail('read after wrong identity'), { ...handshake, service: 'another-service' }); + await assert.rejects(runObserve({ command: 'network' }, { env: f.env }), /handshake/); + assert.equal(f.requests.length, 1); +}); + +test('errors are not retried and do not print credential or server error bodies', async t => { + const f = await fixture(t, (_req, res) => json(res, { code: 'AGENT_INPUT_CONFIG_CHANGED', error: 'private-test-token' }, 409)); + const out = []; + await assert.rejects(runObserve({ command: 'input-config', agentId: 'agent-a' }, { env: f.env, emit: v => out.push(v) }), error => /HTTP 409 AGENT_INPUT_CONFIG_CHANGED/.test(error.message) && !error.message.includes(f.env.DESIRECORE_TOKEN)); + assert.equal(out.length, 0); assert.equal(f.requests.length, 2); +}); + +test('HTML responses and redirects cannot masquerade as successful API reads', async t => { + const f = await fixture(t, (_req, res) => { res.writeHead(200, { 'content-type': 'text/html' }); res.end('login'); }); + await assert.rejects(runObserve({ command: 'network' }, { env: f.env }), /expected JSON/); + const g = await fixture(t, (_req, res) => { res.writeHead(302, { location: '/other' }); res.end(); }); + await assert.rejects(runObserve({ command: 'network' }, { env: g.env }), /fetch failed/); + assert.equal(g.requests.length, 2); +}); + +test('invalid input configuration is not silently treated as automatic input mode', async t => { + const f = await fixture(t, (_req, res) => json(res, { version: 1, agentId: 'agent-a', configRevision: 'a'.repeat(64) })); + await assert.rejects(runObserve({ command: 'input-config', agentId: 'agent-a' }, { env: f.env }), /do not assume manual=false/); +}); + +test('unsupported or unsafe options fail before connecting', async t => { + const f = await fixture(t, () => assert.fail('unexpected read')); + for (const options of [{ command: 'recent-calls', limit: '1x' }, { command: 'network', since: 'bad' }, { command: 'usage', timezone: 'not-a-zone' }, { command: 'input-config', agentId: '../escape' }]) await assert.rejects(runObserve(options, { env: f.env })); + await assert.rejects(runObserve({ command: 'network' }, { env: { ...f.env, DESIRECORE_URL: 'http://remote.example' } })); + assert.equal(f.requests.length, 0); +}); diff --git a/api/examples/agent-client/test/source-observe.fixture.ts b/api/examples/agent-client/test/source-observe.fixture.ts new file mode 100644 index 0000000..4953d38 --- /dev/null +++ b/api/examples/agent-client/test/source-observe.fixture.ts @@ -0,0 +1,267 @@ +/** Copy to packages/agent-service/src/__tests__/api-docs-observe.resource.test.ts. + * Real instance-token auth, routes, input reader, monitor, SessionStore, Mention store and Receipt index. + * Only the local owner profile and runtime discovery are substitutes; all files are synthetic temporary data. + * No Provider, tool executor or outbound network probe is called. + */ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import Fastify, { type FastifyInstance } from 'fastify' +import { appendFileSync, mkdirSync, mkdtempSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs' +import { join } from 'node:path' +import { tmpdir } from 'node:os' +import { createHash } from 'node:crypto' + +vi.mock('../agent/agentfs', async importOriginal => ({ + ...await importOriginal(), + readUserProfile: () => ({ id: 'docs-observe-owner' }), + ensureUserProfile: () => ({ id: 'docs-observe-owner' }), +})) +vi.mock('../runtime', () => ({ getAllActiveSessions: () => [], getRuntime: () => ({ registerPersistentSessionReference: vi.fn() }) })) + +import { instanceAuthOnRequest, INSTANCE_TOKEN_HEADER } from '../auth/instance-auth' +import { registerAgentInputConfigRoute } from '../routes/agent-input-config-route' +import { readAgentInputConfig } from '../agent/input-config' +import { registerNetworkRoutes } from '../routes/network-routes' +import { getNetworkHealthMonitor, resetNetworkHealthMonitor } from '../connection/network-health' +import { DEFAULT_NETWORK_HEALTH_CONFIG } from '@desirecore/schemas/agent-service/network-health' +import { registerHistoryRoutes } from '../routes/history-routes' +import { registerRunsRoutes } from '../routes/runs-routes' +import { getConversationMentionSourceStore, resetConversationMentionSourceStoreForTests } from '../mentions/conversation/source' +import { queryCache, rebuildIndex } from '../session/runs-index' + +const OWNER = 'docs-observe-owner' +const token = 'synthetic-docs-observe-token' +const headers = { [INSTANCE_TOKEN_HEADER]: token } +let root: string +let app: FastifyInstance + +function agent(id = 'observe-agent', manual = false) { + const dir = join(root, 'agents', id) + mkdirSync(dir, { recursive: true }) + const text = JSON.stringify({ name: id, session_mode: { manual } }) + writeFileSync(join(dir, 'agent.json'), text) + return { dir, text, file: join(dir, 'agent.json') } +} +function run(id: string, owner = OWNER, extra: Record = {}, messages?: unknown[]) { + const dir = join(root, 'runs', id) + mkdirSync(dir, { recursive: true }) + writeFileSync(join(dir, 'meta.json'), JSON.stringify({ + run_id: id, user_id: owner, agent_id: 'observe-agent', status: 'completed', + started_at: '2026-10-24T00:00:00.000Z', ...extra, + })) + writeFileSync(join(dir, 'messages.jsonl'), (messages ?? [ + { id: `${id}-user`, role: 'user', content: `synthetic ${id}`, timestamp: 1 }, + { id: `${id}-assistant`, role: 'assistant', content: `answer ${id}`, timestamp: 2 }, + ]).map(value => JSON.stringify(value)).join('\n') + '\n') + return dir +} +function receipt(dir: string, id: string, inputTokens: number, startedAt = '2026-10-25T00:05:00.000Z', usage = true) { + mkdirSync(join(dir, 'receipts'), { recursive: true }) + appendFileSync(join(dir, 'receipts', 'receipts.jsonl'), JSON.stringify({ + receipt_id: `receipt-${id}`, run_id: id, goal: 'SYNTHETIC-PRIVATE-GOAL', input: 'SYNTHETIC-PRIVATE-INPUT', output: 'SYNTHETIC-PRIVATE-OUTPUT', + started_at: startedAt, completed_at: '2026-10-25T00:06:00.000Z', + outcome: { status: 'success', auto_completed: true, user_edit_count: 0 }, tool_calls: [], + ...(usage ? { usage: { input_tokens: inputTokens, output_tokens: 2, cost_usd: 0.01, provider: 'synthetic', model: 'fixture', usage_scope: 'delta' } } : {}), + }) + '\n') +} +async function get(path: string) { + const response = await app.inject({ method: 'GET', url: path, headers }) + expect(response.statusCode, `${path}: ${response.body}`).toBe(200) + return response.json() +} +const historyPath = '/api/sessions/messages/all?agentId=observe-agent&order=asc' + +describe('source-backed public observation contracts', () => { + beforeEach(async () => { + root = mkdtempSync(join(tmpdir(), 'api-docs-observe-')) + vi.stubEnv('DESIRECORE_TEST_ROOT', root) + vi.stubEnv('AGENT_SERVICE_TOKEN', token) + vi.stubEnv('AGENT_SERVICE_MODE', 'standalone') + resetNetworkHealthMonitor() + resetConversationMentionSourceStoreForTests() + queryCache.invalidateAll() + agent() + app = Fastify() + app.addHook('onRequest', instanceAuthOnRequest) + registerAgentInputConfigRoute(app) + await registerNetworkRoutes(app) + await registerHistoryRoutes(app) + await registerRunsRoutes(app) + }) + afterEach(async () => { + await app.close() + resetNetworkHealthMonitor() + resetConversationMentionSourceStoreForTests() + queryCache.invalidateAll() + rmSync(root, { recursive: true, force: true }) + vi.unstubAllEnvs() + }) + + it('uses real instance-token auth on all five reads', async () => { + for (const url of ['/api/agents/observe-agent/input-config', '/api/network/health', historyPath, '/api/runs/recent-calls', '/api/runs/usage/summary']) { + for (const credential of [undefined, 'synthetic-wrong-token']) { + expect((await app.inject({ method: 'GET', url, headers: credential ? { [INSTANCE_TOKEN_HEADER]: credential } : {} })).statusCode).toBe(401) + } + } + }) + it('reads real fresh input config with no-store and a byte-content revision; target 404 and invalid 503 stay distinct', async () => { + const f = agent('observe-agent', true) + const first = await app.inject({ method: 'GET', url: '/api/agents/observe-agent/input-config', headers }) + expect(first.statusCode).toBe(200) + expect(first.headers['cache-control']).toBe('no-store') + expect(first.json()).toEqual({ version: 1, agentId: 'observe-agent', manual: true, configRevision: createHash('sha256').update(f.text).digest('hex') }) + expect(readFileSync(f.file, 'utf8')).toBe(f.text) + writeFileSync(f.file, '{}') + const second = await get('/api/agents/observe-agent/input-config') + expect(second.manual).toBe(false) + expect(second.configRevision).not.toBe(first.json().configRevision) + const missing = await app.inject({ url: '/api/agents/missing-agent/input-config', headers }) + expect(missing.statusCode).toBe(404) + expect(missing.json().code).toBe('AGENT_NOT_FOUND') + const invalidId = await app.inject({ url: '/api/agents/bad.id/input-config', headers }) + expect(invalidId.statusCode).toBe(400) + writeFileSync(f.file, '{bad-json') + const invalid = await app.inject({ url: '/api/agents/observe-agent/input-config', headers }) + expect(invalid.statusCode).toBe(503) + expect(invalid.json().code).toBe('AGENT_INPUT_CONFIG_INVALID') + }) + it('real input reader detects atomic replacement during its FD read instead of returning a mixed revision', async () => { + const f = agent('observe-agent', true) + await expect(readAgentInputConfig('observe-agent', { afterContentRead: () => { + writeFileSync(join(f.dir, 'replacement.json'), '{"session_mode":{"manual":false}}') + renameSync(join(f.dir, 'replacement.json'), f.file) + } })).rejects.toMatchObject({ code: 'AGENT_INPUT_CONFIG_CHANGED' }) + expect((await get('/api/agents/observe-agent/input-config')).manual).toBe(false) + }) + it('network route returns the real current monitor state; disabled remains unknown and new samples update seq', async () => { + const monitor = getNetworkHealthMonitor() + const initial = await get('/api/network/health') + expect(initial).toMatchObject({ status: 'unknown', reason: 'init', seq: 0 }) + monitor.reportSuccess({ source: 'pi-adapter', host: 'synthetic.invalid' }) + expect(await get('/api/network/health')).toEqual(initial) + monitor.setConfig({ ...DEFAULT_NETWORK_HEALTH_CONFIG, enabled: true }) + const events: unknown[] = [] + const unsubscribe = monitor.onChange(state => events.push(state)) + monitor.reportSuccess({ source: 'pi-adapter', host: 'synthetic.invalid' }) + const online = await get('/api/network/health') + expect(online).toMatchObject({ status: 'online', reason: 'request_ok' }) + expect(online.seq).toBeGreaterThan(initial.seq) + expect(events.at(-1)).toEqual(online) + monitor.reportFailure({ source: 'pi-adapter', host: 'one.invalid', subcategory: 'dns' }) + // Cancel timers immediately; only supplied passive evidence is under test, never an outbound probe. + monitor.dispose() + expect(await get('/api/network/health')).toMatchObject({ status: 'degraded', subcategory: 'dns' }) + monitor.reportFailure({ source: 'pi-adapter', host: 'two.invalid', subcategory: 'dns' }) + monitor.dispose() + const offline = await get('/api/network/health') + expect(offline).toMatchObject({ status: 'offline', scope: 'global', reason: 'multi_host_dns' }) + monitor.setConfig({ ...DEFAULT_NETWORK_HEALTH_CONFIG, enabled: false }) + const disabled = await get('/api/network/health') + expect(disabled.status).toBe('unknown') + expect(disabled.seq).toBeGreaterThan(offline.seq) + unsubscribe() + resetNetworkHealthMonitor() + expect((await get('/api/network/health')).seq).toBe(0) + }) + it('merged history filters owner before pagination, accepts limit zero, and rejects stale public revisions', async () => { + const dir = run('owned-history') + run('other-history', 'synthetic-other') + const first = await get(historyPath + '&limit=1') + expect(first.total).toBe(2) + expect(first.items).toHaveLength(1) + expect(first.items[0].runId).toBe('owned-history') + expect(first.historyRevision).toMatch(/^h1:[0-9a-f]{64}$/) + const zero = await get(historyPath + '&limit=0') + expect(zero).toMatchObject({ items: [], total: 2, hasMore: false, historyRevision: first.historyRevision }) + expect((await get(historyPath + '&limit=1000')).items).toHaveLength(2) + const next = await get(historyPath + '&offset=1&limit=1&expectedRevision=' + first.historyRevision) + expect(next.items[0].message.id).toBe('owned-history-assistant') + writeFileSync(join(dir, 'messages.jsonl'), JSON.stringify({ id: 'edited', role: 'assistant', content: 'synthetic edited content', timestamp: 1 }) + '\n') + const stale = await app.inject({ url: historyPath + '&expectedRevision=' + first.historyRevision, headers }) + expect(stale.statusCode).toBe(409) + expect(stale.json().error).toBe('history_revision_changed') + expect((await get(historyPath)).historyRevision).not.toBe(first.historyRevision) + for (const query of ['&limit=-1', '&limit=1001', '&offset=-1', '&expectedRevision=bad']) { + expect((await app.inject({ url: historyPath + query, headers })).statusCode).toBe(400) + } + }) + it('manual mode excludes other manual conversations until explicitly selected or disabled', async () => { + agent('observe-agent', true) + run('default-history') + run('manual-history', OWNER, { conversation_id: '__manual:synthetic-topic' }) + const current = await get(historyPath) + expect(new Set(current.items.map((item: { runId: string }) => item.runId))).toEqual(new Set(['default-history'])) + const explicit = await get(historyPath + '&conversationId=__manual%3Asynthetic-topic') + expect(new Set(explicit.items.map((item: { runId: string }) => item.runId))).toEqual(new Set(['manual-history'])) + agent('observe-agent', false) + const combined = await get(historyPath) + expect(new Set(combined.items.map((item: { runId: string }) => item.runId))).toEqual(new Set(['default-history', 'manual-history'])) + }) + it('preferred Run selects an older fork view instead of mixing siblings', async () => { + // Forks are written by SessionStore as camelCase snapshots; never mix identity spellings. + for (const [id, day] of [['fork-source', '22'], ['fork-old', '23'], ['fork-new', '24']]) { + const dir = run(id) + writeFileSync(join(dir, 'meta.json'), JSON.stringify({ + runId: id, userId: OWNER, agentId: 'observe-agent', status: 'completed', + engine: 'desirecore-engine', options: {}, messageCount: 2, + createdAt: `2026-10-${day}T00:00:00.000Z`, updatedAt: `2026-10-${day}T00:00:00.000Z`, + ...(id !== 'fork-source' && { forkedFrom: 'fork-source' }), + })) + } + const canonical = await get(historyPath) + expect(new Set(canonical.items.map((item: { runId: string }) => item.runId))).toEqual(new Set(['fork-new'])) + const selected = await get(historyPath + '&preferredRunId=fork-old') + expect(new Set(selected.items.map((item: { runId: string }) => item.runId))).toEqual(new Set(['fork-old'])) + expect(selected.historyRevision).not.toBe(canonical.historyRevision) + }) + it('merged history applies normal public privacy and preserves sensitive failure facts', async () => { + run('privacy-history', OWNER, {}, [{ role: 'assistant', content: '', timestamp: 1, + providerReplay: { version: 1, turns: [{ blocks: [{ type: 'thinking', thinking: 'SYNTHETIC-PRIVATE-REPLAY' }] }] }, + toolUse: [{ id: 'sensitive', name: 'BrowserScript', summary: 'SYNTHETIC-PRIVATE-RESULT', status: 'failed', isError: true, summaryRef: { filename: 'synthetic-private.txt', size: 1 }, summaryIncomplete: true }], + }]) + const page = await get(historyPath) + const tool = page.items[0].message.toolUse[0] + expect(tool).toMatchObject({ status: 'failed', isError: true }) + expect(tool.summary).toContain('failed') + expect(tool).not.toHaveProperty('summaryRef') + expect(tool).not.toHaveProperty('summaryIncomplete') + expect(JSON.stringify(page)).not.toMatch(/SYNTHETIC-PRIVATE|synthetic-private\.txt|providerReplay/) + }) + it('includes persisted external-only Mention source with a synthetic non-Run identifier and owner isolation', async () => { + const store = getConversationMentionSourceStore() + for (const userId of [OWNER, 'synthetic-other']) await store.persist({ + userId, currentAgentId: 'observe-agent', conversationId: 'conversation-source', messageId: `mention-${userId}`, + content: '@Builder synthetic review', actor: { type: 'human', id: userId }, + occurrences: [{ occurrence_id: `occurrence-${userId}`, target: { agent_id: 'builder-agent', display_name_snapshot: 'Builder' }, range: { start: 0, end: 8 } }], + recipientRequests: [{ occurrence_ids: [`occurrence-${userId}`], recipient_agent_id: 'builder-agent', route: 'external_agent', context_selection: { context_mode: 'isolated', origin: 'mention', intent_id: `intent-${userId}`, context_reason: 'Synthetic external review' } }], + idempotencyKey: `key-${userId}`, correlationId: `correlation-${userId}`, now: '2026-10-25T00:00:00.000Z', + }) + const page = await get(historyPath) + expect(page.total).toBe(1) + expect(page.items[0]).toMatchObject({ runId: `__mention_source:mention-${OWNER}`, messageIndex: 0, message: { mentionSourceOnly: true } }) + expect(JSON.stringify(page)).not.toContain('synthetic-other') + }) + it('usage reads real owner receipts and index, ignores invalid filters, and maps UTC day buckets without returning bodies', async () => { + receipt(run('old-owner'), 'old-owner', 10) + receipt(run('new-owner', OWNER, { started_at: '2026-10-25T00:00:00.000Z', cleared: true }), 'new-owner', 20) + receipt(run('other-owner', 'synthetic-other'), 'other-owner', 1000) + receipt(run('no-usage'), 'no-usage', 1000, undefined, false) + await rebuildIndex() + const utc = await get('/api/runs/usage/summary?timezone=UTC') + expect(utc.total).toMatchObject({ inputTokens: 30, outputTokens: 4, queryCount: 2 }) + expect(utc.byDate[0].date).toBe('2026-10-25') + expect(JSON.stringify(utc)).not.toMatch(/SYNTHETIC-PRIVATE|other-owner/) + expect(await get('/api/runs/usage/summary?timezone=invalid%2Ftimezone')).toEqual(utc) + expect(await get('/api/runs/usage/summary?since=invalid')).toEqual(utc) + const filtered = await get('/api/runs/usage/summary?since=2026-10-25T00%3A00%3A00Z&timezone=UTC') + expect(filtered.total.inputTokens).toBe(20) + // Both receipts occur on Oct 25, but since filters their parent Run start, not receipt time. + expect(filtered.total.queryCount).toBe(1) + const farEast = await get('/api/runs/usage/summary?timezone=Pacific%2FKiritimati') + expect(farEast.byDate[0].date).toBe('2026-10-26') + expect(farEast.total).toEqual(utc.total) + receipt(run('cache-added'), 'cache-added', 100) + expect((await get('/api/runs/usage/summary?since=invalid')).total).toEqual(utc.total) + queryCache.invalidateAll() + expect((await get('/api/runs/usage/summary?since=invalid')).total.inputTokens).toBe(130) + }) +}) diff --git a/api/observation-verification-2026-10-25.md b/api/observation-verification-2026-10-25.md new file mode 100644 index 0000000..937901a --- /dev/null +++ b/api/observation-verification-2026-10-25.md @@ -0,0 +1,78 @@ +# 观察接口与聚合历史接入验证(2026-10-25) + +## 交付范围 + +新增5个HTTP操作:输入配置、网络状态、合并历史、近期工具调用、用量汇总。Agent Service共32操作,Compat3、Workflow5,共40操作;11组中英文页面。新增observe读取示例,history增加all命令,两者均与静态下载逐字对应。 + +本记录与2026-10-22的35操作稳定性复核分开。旧生产、下载与搜索验收保留原始版本,不表示本批已经发布或收录。 + +## 固定来源与依赖 + +- 公开文档开始基线:`c76753613425d236d54360b111efb708b1349131`。 +- 源码开始基线:`a9414962f28696f30bfa1e29aff0a03c4d58a5e9`。 +- 本批固定源码:`a0186f6feeaf66eb88787e8b80b8fea5559b8761`,[源码PR #5194](https://github.com/desirecore/desirecore/pull/5194)。该快照已更新至后续dev基线;所选修复文件与最初通过本地验证的`3131e2d4900bce418abe1bb0a0569148ef901682`逐字一致。内部文档基线`f8e1005d1cdfbeae37d3374de8631307b3d94534`已包含必要内部文档PR #908。规范与catalog引用同一源码提交;Git合入不等于客户端安装包已发行。 +- Compat与Workflow无实质合同改动,规范保留自身来源和既有验证记录,没有伪改成最新提交实跑。 + +## 源码合同及修复 + +近期调用的ID、字段和索引兼容由同一Receipt投影维护:有invocation_id优先使用,否则按Run/稳定Receipt身份/工具序号形成不透明标识;工具状态和时间优先,旧回执可回退整轮事实;缺少耗时为0并不代表实际测得零耗时。公开读取不包含工具输入或结果正文。 + +调用索引升级必须处理旧版本、重建期间增量及旧回执格式,不能只修改新增行。独立审阅发现的小limit选取、同时间不同Receipt重放去重、旧receipt.json混合读取、缺时间回退均作为本批收口条件;最终修复与测试结果在完成后记录,不以最初单Run大limit夹具代替这些边界。 + +聚合历史经过可信owner过滤、分支和手动会话范围选择、Conversation Mention来源合并及公共投影后计算revision,再分页。limit=0为空页元数据;revision冲突409只重读,合成Mention来源runId不可用于执行或恢复。 + +用量是有窗口和缓存的已记录成本,不是供应商账单。since按Run开始时间筛选,byDate以UTC日桶中午映射时区,不能宣称精确逐事件本地日结。Schema及中英说明一同收窄此精度承诺。 + +## 验证分层 + +### 实际源码路由夹具 + +维护夹具:`api/examples/agent-client/test/source-observe.fixture.ts`。复制到固定源码的`packages/agent-service/src/__tests__/api-docs-observe.resource.test.ts`后以Vitest resource project运行,结束移除临时副本。 + +已通过10项: + +- 真实instanceAuth对5条路径的缺失/错误令牌均401。 +- input-config真实文件读取、摘要、no-store、缺省manual、非法ID400、目标业务404、坏配置503;reader的原子替换竞争与已存在的409路由映射测试分别验证。 +- 真实NetworkHealthMonitor的四态、disabled、seq/新monitor重置,以及HTTP读取当前对象;主动探测timer被取消,不外连。 +- SessionStore与HTTP合并历史的owner、limit0/1000、非法页、revision变化409、manual切换、fork选择、敏感失败结果占位与引用移除。 +- 真实Conversation Mention来源store生成source-only项,另一个owner不可读。 +- 真实Receipt及用量索引/回退:owner、无usage、cleared记录、Run开始筛选、invalid参数、UTC桶/UTC+14、缓存失效对照。 + +仅替换合成本机profile、runtime发现与引用登记边界。数据全部在临时DESIRECORE_TEST_ROOT,不读取个人历史。Fastify inject覆盖真实应用HTTP处理与序列化,不等于真实TCP listener、公网部署或商业Provider验收。 + +### 可运行示例 + +history测试含同revision分页、409丢弃已缓冲页后有限重读、连续冲突停止、limit0/上界、显式后页revision、source-only不被当成Run、无执行回放。observe测试覆盖握手身份、真实本机HTTP请求/认证头、四命令路由、可选字段保留、拒绝重定向/HTML、错误不重试和不输出错误体中的凭据。 + +完整Node示例测试与站点检查的最终数量、构建及规范验证结果在完成后补记。测试HTTP服务为本地合成响应,不与源码路由夹具混计,也不代表调用真实业务实例。 + +## Worker专项:未公开 + +Worker专项独立运行真实注册HTTP路由、recorder与reader,共12项隔离检查通过,其中部分断言用于复现当前不满足公共读取门槛的行为,并非全部隐私检查通过。 + +已验证父Run owner的401/404、无worker空记录、七态/unknown、按成功解析entry位置分页、坏行计数及录制上限。尚需修复的实际条件: + +1. sensitiveResult工具的部分进度可先写入早期append-only entry;即使final摘要已占位,HTTP仍可返回该早期进度。 +2. 旧结果与引用没有按当前公共历史规则重新投影;视觉/二进制工具输入和额外JSON字段的公开边界仍需明确。 +3. seq不是HTTP offset游标,truncated是录制达到上限而非hasMore;独立Schema/文档需要校准。 + +正常当前final的成功/失败结果与敏感输入已有保护,父owner校验没有被绕过。这里证明的是合成夹具条件,不是实际用户数据泄漏、跨租户漏洞或真实事故。本批不把两操作加入公开文档、清单或OpenAPI,也不从既有HistoryMessage推导Worker保证。 + +## 验收边界 + +本文件记录提交PR前的本地核验,不替代部署后验收。安装包、商业模型、真实邮箱/S3、压力或SLA不在本次验证范围内。发布时仍需检查实际页面、14份双语下载与DocSearch重抓结果;现有每日抓取计划保持独立。 + + +## 本地验证完成结果 + +- 最近调用修复:resource 3文件56项(recent-tool-calls 12、runs-activity-scope 25、runs-index-empty-owner 19);unit 4文件40项(runs-index 28、reader-receipt-id 3、operations-activity 7、recent-tool-calls 2),共96项通过。 +- input-config既有route/reader专测2文件13项通过,包含409与503错误映射;独立source-observe资源夹具10项通过。三层合计119项源码定向检查,不等于119个端点或完整产品验收。 +- 独立修后复核4个原反例通过;旧JSON原文身份重复重建一致;字节、条数、分片预算耗尽均返回null,不返回已读部分topK。回退仍为有限owner窗口,未宣称两条链路能枚举相同的全历史。 +- Node示例4文件54项通过;源与下载镜像一致;docs typecheck通过(私有源码执行的fixture按既有约定从站点TS范围排除)。 +- 源码13个类型边界及消费闭包、lint:boundaries通过;ADR引用与代码声明检查通过(保留既有未锚定声明提示)。新worktree子模块已初始化并fetch,未修改原源码工作区或关联仓库的既有未推送配置。 +- api:check与双语build、api:check --build通过:11组双语页面、40操作、76个JSON块。AJV Draft7校验75组件Schema、57个规范JSON示例;Redocly2.63.0三规范0错误,保留既有47条条件Schema告警及3条license告警。 +- 浏览器检查6个中英文页面/锚点、1280宽度无整页横向溢出;390宽度检查英文观察表格与中文合并历史,表格独立横向滚动,无整页溢出。原viewport已恢复。 + +源码提交前刷新dev新增的#5193仅修改Task/Plan描述与工具元数据,未更改本批读取接口;rebase的range-diff确认修复补丁不变。最终固定SHA以catalog与Agent Service x-source为准。必要内部合同已按子模块流程单独合并到内部文档main。后续rebase仅移除与dev重复的子模块指针差异,所选修复内容没有丢弃。 + +最终本地构建通过后,实际HTTP读取14份中英文下载文件,全部返回200且逐字匹配static源文件(包含固定源码提交的Agent Service规范);已检查的6个浏览器页面内容与锚点正确。该项是本地预览结果;线上部署和搜索需要另行验收。 diff --git a/docs/07-api/01-connection-and-auth.md b/docs/07-api/01-connection-and-auth.md index 9f9a29b..9759e00 100644 --- a/docs/07-api/01-connection-and-auth.md +++ b/docs/07-api/01-connection-and-auth.md @@ -154,3 +154,5 @@ Cache-Control: no-store - [Agent Webhook](./02-agent-webhook.md):通过目标 Agent 的专用 secret 触发事件。 - [错误与排查](./03-errors-and-troubleshooting.md):处理认证、启动、请求和异步执行问题。 + +- [实例诊断与用量](./10-observability.md):区分实例连接、上游网络与已记录用量。 diff --git a/docs/07-api/03-errors-and-troubleshooting.md b/docs/07-api/03-errors-and-troubleshooting.md index f0e81e2..a7f11fd 100644 --- a/docs/07-api/03-errors-and-troubleshooting.md +++ b/docs/07-api/03-errors-and-troubleshooting.md @@ -109,3 +109,7 @@ Webhook 的 `200` 和 `accepted: true` 表示已接收触发。响应中没有 R HTTP `200` 表示读取成功,不保证返回完整的工具结果正文。敏感结果或无法核验调用身份的结果可能只提供占位提示,并省略正文引用;这与执行失败、历史损坏或需要重跑不是一回事。保留实际的 `status`、`isError`,按[历史结果的读取边界](./08-session-history-and-messages.md#history-result-privacy)处理,不从占位文字推断成功或自动重试。 逐 Run 消息中的 `offset` 是字节位置,而分页请求的 `offset` 是源行位置;按请求窗口推进,不能用返回消息条数补算下一页。历史可能在读取期间变化,参见[会话历史与消息](./08-session-history-and-messages.md)。 + +## 网络连接正常但上游不可用 + +Socket.IO 已连接并不保证实例可访问模型供应商。先读取[上游网络状态](./10-observability.md#network-health),结合 `reason`、`since` 和供应商范围排查;`unknown` 不是离线,历史 `online` 也不证明当前请求成功。近期调用和用量只是观察信息,不据此自动重放执行。 diff --git a/docs/07-api/04-conversations-and-runs.md b/docs/07-api/04-conversations-and-runs.md index f67b0a6..dcd58e9 100644 --- a/docs/07-api/04-conversations-and-runs.md +++ b/docs/07-api/04-conversations-and-runs.md @@ -12,7 +12,7 @@ keywords: [API, 智能体, Run, Receipt, 对话, 运行状态, 分页] ## 读取范围 -智能体列表、Run 列表、Run 详情和运行态供实例所有者读取实例数据,不能作为多个租户之间逐用户隔离的接口。回执读取另核对服务端解析的可信用户身份与 Run 的用户归属;Run 不存在或不属于该用户时均返回 `404`。 +智能体列表、输入配置、Run 列表、Run 详情和运行态供实例所有者读取实例数据,不能作为多个租户之间逐用户隔离的接口。回执读取另核对服务端解析的可信用户身份与 Run 的用户归属;Run 不存在或不属于该用户时均返回 `404`。 Agent ID、Run ID、`user_id` 和 `conversation_id` 是标识信息,不是访问凭据。请求中自报用户 ID、知道 Run ID 或曾在别处看到一条记录,都不能代替认证与对应接口的归属检查。 @@ -58,6 +58,50 @@ curl --silent --show-error \ | `ui`、`avatar`、`avatarImage` | 可选界面与头像摘要;头像配置存在不等于对应文件可用 | | `unread`、`pinned`、`pinnedAt`、`folded`、`groupOverride`、`nestMembers` | 可选的当前实例用户未读信息与显示偏好,缺席时不要自行补成已观测值 | +## 读取智能体输入配置 {#agent-input-config} + +### `GET /api/agents/:id/input-config` + +:::info 生效版本 +`v10.0.180+`(读取智能体输入配置)。 +::: + +选中智能体后,用此接口取得轻量输入配置,判断它当前是否使用手动独立会话模式。`:id` 使用智能体列表返回的 `id`,仅含字母、数字、连字符或下划线;没有请求体或查询参数。读取不会创建会话或发送问候。 + +```bash +curl --silent --show-error \ + --header "X-Agent-Service-Token: ${AGENT_SERVICE_TOKEN}" \ + "${BASE_URL}/api/agents/${AGENT_ID}/input-config" +``` + +```json +{ + "version": 1, + "agentId": "your-agent-id", + "manual": true, + "configRevision": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" +} +``` + +| 字段 | 含义 | +|---|---| +| `version` | 当前为整数 `1`;未知版本应提示不兼容,不要自行按普通会话模式继续 | +| `agentId` | 实际读取的智能体 ID;应与所选目标一致 | +| `manual` | `true` 表示手动独立会话模式;合法配置未声明该模式时为 `false` | +| `configRevision` | 64 位小写十六进制内容摘要;只比较是否相等,不按大小或时间排序 | + +响应使用 `Cache-Control: no-store`。切换目标或准备输入时重新读取;`configRevision` 不锁定后续配置,也不是执行授权或递增事件序号。`manual: false` 只说明输入模式,不能据此跳过实际执行的权限与状态检查。读取失败时保留“配置不可用”,不要补成 `manual: false`。 + +| HTTP / `code` | 处理方式 | +|---|---| +| `400 AGENT_INPUT_CONFIG_INVALID_ID` | 使用所选智能体的真实 ID | +| `404 AGENT_NOT_FOUND` | 目标不可用或不存在;重新选择智能体。这是业务错误,不代表旧服务缺少此接口 | +| `409 AGENT_INPUT_CONFIG_CHANGED` | 读取期间配置发生变化;重新读取后再更新输入界面 | +| `503 AGENT_INPUT_CONFIG_INVALID` | 配置格式无效,请管理员修复智能体配置 | +| `503 AGENT_INPUT_CONFIG_UNREADABLE` | 配置暂不可读,请管理员检查后重试读取 | + +缺少路由的旧服务可能也返回 `404`;结合 JSON 中的 `code`、握手和客户端版本判断,不要把所有 `404` 都解释为可安全降级。认证和服务未就绪错误见[错误与排查](./03-errors-and-troubleshooting.md)。可运行调用见[观察接口示例](./10-observability.md#observe-example)。 + ## 查询 Run 列表 {#list-runs} ### `GET /api/runs` diff --git a/docs/07-api/07-openapi.md b/docs/07-api/07-openapi.md index cd45e37..fbee975 100644 --- a/docs/07-api/07-openapi.md +++ b/docs/07-api/07-openapi.md @@ -26,6 +26,11 @@ sidebar_position: 7 | `POST /api/agents/{id}/webhooks/secret/rotate` | 轮换智能体的专用 Webhook 密钥 | [智能体 Webhook](./02-agent-webhook.md) | | `GET /api/agents/{agentId}/schedule-messages` | 读取可见的后台消息与交付 | [智能体 Webhook](./02-agent-webhook.md) | | `GET /api/agents` | 读取智能体摘要列表 | [智能体列表](./04-conversations-and-runs.md#list-agents) | +| `GET /api/agents/{id}/input-config` | 读取轻量输入模式和配置摘要 | [输入配置](./04-conversations-and-runs.md#agent-input-config) | +| `GET /api/network/health` | 读取上游网络观察状态 | [实例诊断与用量](./10-observability.md#network-health) | +| `GET /api/runs/recent-calls` | 读取当前用户近期工具调用元数据 | [近期工具调用](./10-observability.md#recent-tool-calls) | +| `GET /api/runs/usage/summary` | 读取当前用户已记录用量汇总 | [已记录用量](./10-observability.md#usage-summary) | +| `GET /api/sessions/messages/all` | 按版本分页读取合并历史 | [合并历史](./08-session-history-and-messages.md#merged-history) | | `GET /api/runs` | 按条件读取实例运行记录 | [运行列表](./04-conversations-and-runs.md#list-runs) | | `GET /api/runs/{id}` | 读取单个运行的元信息 | [运行详情](./04-conversations-and-runs.md#get-run) | | `GET /api/runs/{id}/runtime-state` | 读取运行的原始状态 | [运行状态](./04-conversations-and-runs.md#get-run-runtime-state) | @@ -100,3 +105,5 @@ Socket.IO 的连接、订阅与异步事件需要配套客户端逻辑。OpenAPI 文件的 `info.version` 是这份 HTTP 接口描述的版本。它不代表 DesireCore 客户端的生效版本,也不代替握手中的 `serverVersion` 或 `apiProtocolVersion`。 媒体原生播放使用签发端点返回的完整 `/media/{filename}?expires=…&signature=…` 链接;该读取入口不使用实例令牌,链接应按短期凭据保管。附件下载仍使用实例认证。历史、话题和媒体的权限与响应范围见各自指南。 + +观察和聚合历史的可运行读取示例分别为 **[observe.mjs](pathname:///api-examples/observe.mjs)** 与 **[history.mjs](pathname:///api-examples/history.mjs)**。聚合历史的 revision 冲突需要重新读取,不能混拼旧页;用量聚合不等于供应商最终账单。 diff --git a/docs/07-api/08-session-history-and-messages.md b/docs/07-api/08-session-history-and-messages.md index 5e6b8d0..dba059b 100644 --- a/docs/07-api/08-session-history-and-messages.md +++ b/docs/07-api/08-session-history-and-messages.md @@ -12,7 +12,7 @@ keywords: [API, 会话历史, 消息, 搜索, 话题, 分页, Run] ## 读取范围与标识 {#history-scope} -会话列表、搜索和精确 Run 历史按服务端解析的用户归属读取。精确 Run 读取在返回正文前检查该归属;不存在、不属于该用户或已经清除的 Run 均按 `404` 处理。知道 Run ID 不等于有读取权限,也不能通过请求参数自报 `userId` 来选择其他用户。 +会话列表、合并历史、搜索和精确 Run 历史按服务端解析的用户归属读取。精确 Run 读取在返回正文前检查该归属;不存在、不属于该用户或已经清除的 Run 均按 `404` 处理。知道 Run ID 不等于有读取权限,也不能通过请求参数自报 `userId` 来选择其他用户。 两个话题接口读取所连接实例当前本机用户资料所属的智能体对话记忆。它们适用于实例所有者,不能作为共享服务的多租户隔离接口。资料缺失时返回 `401`;请实例管理员检查当前用户资料。 @@ -165,6 +165,81 @@ curl --silent --show-error --get \ `live` 是可替换的临时显示快照,可能回退,也可能在终态或最终消息落盘后消失;它不计入 `total`,不能当作持久消息、回执或成功结论。只包含普通输出文本和有限工具状态;不要据此恢复工具输入或执行。`text` 最多 16,384 字符,`toolCalls` 最多 128 项。 +## 读取合并历史 {#merged-history} + +### `GET /api/sessions/messages/all` + +需要展示一个智能体或一个对话的连续可见时间线时,使用此接口。它先按可信用户归属筛选,再选择 fork 家族的可见分支、合并来源消息并做公开投影,最后分页。它与逐 Run 消息的源行分页不同;`all` 也不表示所有执行分支或完整备份。 + +| 查询参数 | 含义 | +|---|---| +| `agentId` | 必填,目标智能体 ID | +| `conversationId` | 可选,精确限定该智能体下有权读取的对话谱系,例如已有的 `__manual:…` 会话 ID;不授予其他对话的读取权限 | +| `preferredRunId` | 可选,选择可见历史分支窗口;不是只返回该 Run,也不是访问凭据 | +| `order` | `desc`(默认,新到旧)或 `asc`;其他值按 `desc` 处理 | +| `limit` | 默认 `100`,范围 `0`–`1000`;`0` 仅返回空 `items`、总数与版本,且 `hasMore: false`,不表示取全部 | +| `offset` | 默认 `0`,完整合并视图中的条目位置;非负安全整数 | +| `expectedRevision` | 后续页携带首屏的 `historyRevision`,格式为 `h1:` 加 64 位小写十六进制字符 | + +`limit` 与 `offset` 请使用规范十进制整数。服务兼容解析整数前缀,不应依赖混杂字符串或小数的转换结果。安全整数最大为 `9007199254740991`。 + +不传 `conversationId` 时,手动模式仍开启的智能体不合并独立手动会话;关闭手动模式后,已有手动会话会合并回默认时间线。传入 `conversationId` 时只读取该对话。已清除、已归档、委派子 Run、调度与 Webhook 专用执行时间线不会纳入;Mention 的独立执行时间线也不混入,但可见的 Conversation 来源消息可以出现。无可见历史时返回空页,不表示智能体不存在,也不会泄露他人的记录。 + +```bash +curl --silent --show-error --get \ + --header "X-Agent-Service-Token: ${AGENT_SERVICE_TOKEN}" \ + --data-urlencode "agentId=${AGENT_ID}" \ + --data-urlencode 'order=asc' --data-urlencode 'limit=100' \ + --data-urlencode 'offset=0' \ + "${BASE_URL}/api/sessions/messages/all" +``` + +```json +{ + "items": [ + { + "runId": "run-example", + "messageIndex": 0, + "offset": 0, + "sessionTime": "2026-10-15T08:00:00Z", + "message": { + "id": "d8d53a7a-478e-4d96-970e-657dece814f7", + "role": "user", + "content": "Please review the release notes", + "timestamp": 1792051200000 + } + } + ], + "total": 1, + "hasMore": false, + "historyRevision": "h1:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" +} +``` + +`items` 每项封装 `runId`、`messageIndex`、`sessionTime` 与公开 `message`,持久化 Run 消息还可带字节 `offset`。这些项内位置不是下一页参数;下一页仍使用**请求的 `offset + limit`**,根据 `hasMore` 决定是否继续。`total` 计合并后的公开条目,不是各 Run 源行数之和。 + +**Mention 来源项**可能带 `message.mentionSourceOnly: true`,其外层 `runId` 为 `__mention_source:…`,没有真正的 Run 归属,也可能没有字节 `offset`。保留消息的 Mention 引用供显示;不要把合成 ID 传给 Run、续聊或恢复接口,不要把来源项补入模型回放。关联消息优先使用 `message.id`,不要跨所有 Run 仅按文字去重。 + +### 保持同一公开视图版本 + +1. 从 `offset=0` 开始,保存首屏 `historyRevision`。 +2. 后续页保持相同的 `agentId`、`conversationId`、`preferredRunId`、`order` 和页大小,携带 `expectedRevision`。 +3. 在所有页读完之前,暂存该轮结果。收到 `409 history_revision_changed` 时丢弃整轮已暂存页,从 `offset=0` 重新读取并取得新版本;不要拼接新旧页。 +4. 只重试读取。不要因历史变化、敏感结果占位或读取错误重新提交对话、重跑工具或发起恢复。 + +```bash +# HISTORY_REVISION 为首屏响应的完整 historyRevision;其他筛选与首屏一致。 +curl --silent --show-error --get \ + --header "X-Agent-Service-Token: ${AGENT_SERVICE_TOKEN}" \ + --data-urlencode "agentId=${AGENT_ID}" \ + --data-urlencode 'order=asc' --data-urlencode 'limit=100' \ + --data-urlencode 'offset=100' \ + --data-urlencode "expectedRevision=${HISTORY_REVISION}" \ + "${BASE_URL}/api/sessions/messages/all" +``` + +正文编辑、删除、分支或公开投影变化都可能使版本失效。`historyRevision` 是当前完整公开视图的摘要,不是持久游标、事务快照或增量日志。以后再次读取仍需重新确认版本。每页可能读取并排序完整可见历史,大历史应使用有限页数,避免无界并发读取。 + ## 搜索一个 Agent 的消息 {#search-messages} ### `GET /api/sessions/messages/search` @@ -273,24 +348,30 @@ node history.mjs list --limit 20 --all-pages node history.mjs session run-example node history.mjs messages run-example --limit 100 --all-pages node history.mjs messages delegated-run-id --inspect-run +node history.mjs all --order asc --limit 100 --all-pages +node history.mjs all --conversation-id '__manual:existing-conversation' --limit 0 node history.mjs search --query 'release' --preview-chars 240 --all-pages node history.mjs overview node history.mjs matter mat_example ``` -示例先验证实例握手,再发起 GET;不会提交查询、续聊或撤销运行。默认每个命令只读一页,`--all-pages` 最多读取 20 页,可用 `--max-pages` 调整。输出为逐行 JSON,包含历史正文,请按私密数据保存。 +示例先验证实例握手,再发起 GET;不会提交查询、续聊或撤销运行。默认每个命令只读一页,`--all-pages` 每次尝试最多读取 20 页,可用 `--max-pages` 调整。输出为逐行 JSON,包含历史正文,请按私密数据保存。 + +`list` 用 `--include-archived` 包含归档;`search` 另支持 `--type`、`--preferred-run-id` 和 `--preview-chars`。`messages` 的检查视图仅通过显式 `--inspect-run` 开启。`all` 支持 `--conversation-id`、`--order` 和 `--preferred-run-id`;后续单页用 `--offset` 与 `--expected-revision` 配对读取。 -`list` 用 `--include-archived` 包含归档;`search` 另支持 `--type`、`--preferred-run-id` 和 `--preview-chars`。`messages` 的检查视图仅通过显式 `--inspect-run` 开启。 +退出码 `0` 表示请求的读取成功,`1` 表示配置或 HTTP 请求失败,`2` 表示翻页达到设定上限、观测到总数变化或持续版本冲突而停止,或话题响应被截断。后续读取位置见停止记录中的 `nextOffset`;`all` 同时保留 `historyRevision`。版本冲突停止记录给出 `restartOffset: 0`,不能从旧位置继续。 -退出码 `0` 表示请求的读取成功,`1` 表示配置或 HTTP 请求失败,`2` 表示翻页达到设定上限、观测到总数变化而停止,或话题响应被截断。后续读取位置见停止记录中的 `nextOffset`;重新开始前应确认历史是否变化。 +`all --all-pages` 暂存当前尝试,版本冲突时丢弃后从首屏重读,默认最多重启一次(`--max-restarts` 可调整),每次仍受 `--max-pages` 限制。成功后输出同一版本的页;达到页数上限时输出带版本的部分窗口并以 `2` 退出。`all --limit 0` 只读取一次元信息。 -这些接口没有提供跨页一致性版本。执行中追加、编辑或清理历史,都可能让多次读取出现重复或遗漏。示例发现 `total` 变化会停止,但不能检测总数不变的内容改写;不要把一次分页扫描当成固定时点的审计快照。 +其余分页接口没有跨页一致性版本。执行中追加、编辑或清理历史,都可能让多次读取出现重复或遗漏。示例发现 `total` 变化会停止,但不能检测总数不变的内容改写;不要把一次分页扫描当成固定时点的审计快照。 ## 常见错误 {#history-errors} | HTTP / 错误 | 处理方式 | |---|---| -| `400 agentId is required` | 搜索时提供真实 Agent ID | +| `400 agentId is required` | 合并历史或搜索时提供真实 Agent ID | +| `400 invalid_history_page` | 合并历史的 `limit` 使用 `0`–`1000`,检查非负安全整数 `offset` 和完整 `expectedRevision` 格式 | +| `409 history_revision_changed` | 丢弃该轮所有页,从首屏重新读取;不要重跑执行 | | `400 Invalid history page parameters` | 搜索的 `limit`、`offset` 使用非负安全整数,并检查 `previewChars` 范围 | | `400 Invalid runId` | 使用会话列表返回的 Run ID,不传路径或运行时 session ID | | `401` | 检查实例令牌;若为 `User not authenticated` 或 `User not found`,请管理员检查当前实例用户资料 | diff --git a/docs/07-api/10-observability.md b/docs/07-api/10-observability.md new file mode 100644 index 0000000..31a2ea1 --- /dev/null +++ b/docs/07-api/10-observability.md @@ -0,0 +1,176 @@ +--- +title: 实例诊断与用量 +description: 读取实例的上游网络状态、近期工具调用和已记录用量,区分连接、执行结果与费用统计。 +slug: /api/observability +sidebar_position: 10 +keywords: [API, 网络状态, 工具调用, 用量, Token, 诊断] +--- + +# 实例诊断与用量 + +部署所有者可以使用已有实例地址和 `X-Agent-Service-Token` 读取以下观察信息。先完成[连接验证](./01-connection-and-auth.md),再根据实际问题选择接口。 + +| 你需要判断什么 | 接口 | 不能据此推断什么 | +|---|---|---| +| 实例最近观察到的上游网络状况 | `GET /api/network/health` | 某个模型、工具或请求已经成功 | +| 当前用户最近的工具调用概览 | `GET /api/runs/recent-calls` | 全量审计历史、某轮对话已完成 | +| 当前用户已记录的 Token 和成本 | `GET /api/runs/usage/summary` | 供应商最终账单或精确本地日结 | + +这些请求不提交对话,也不重放工具。它们读取的范围、时间和统计方式不同,不能拼成一次请求的完成证明。请求与终态关联仍按[对话与运行记录](./04-conversations-and-runs.md#request-reconciliation)处理。 + +## 上游网络状态 {#network-health} + +### `GET /api/network/health` + +无请求体和查询参数。成功直接返回状态对象: + +```bash +curl --silent --show-error \ + --header "X-Agent-Service-Token: ${AGENT_SERVICE_TOKEN}" \ + "${BASE_URL}/api/network/health" +``` + +```json +{ + "status": "unknown", + "reason": "init", + "since": "2026-10-25T00:00:00.000Z", + "seq": 0 +} +``` + +| 字段 | 含义 | +|---|---| +| `status` | `online`:近期上游请求或探测成功;`degraded`:有网络失败或部分供应商不可达;`offline`:网络不可达已有探测等证据;`unknown`:尚无足够样本,例如启动或唤醒后 | +| `reason` | `init`、`request_ok`、`probe_ok`、`request_failed`、`probe_failed`、`stream_stall`、`multi_host_dns`、`suspend_resume` | +| `since` | 进入当前 `status` 的 ISO 时间;相同状态的原因变化不一定更新该时间 | +| `seq` | 当前网络监控实例的递增序号,用于比较快照和迟到事件;不是跨服务重启的持久版本 | +| `scope` | 可选 `global` 或 `provider`,仅对异常状态有意义 | +| `affectedProviders` | 可选供应商 ID 数组,配合 `scope: provider` 使用 | +| `subcategory` | 可选 `dns`、`tls`、`tcp_reset`、`refused`、`unreachable`、`timeout`、`stream_interrupted`,帮助定位网络问题 | + +Socket.IO 连接正常只说明调用方与实例之间可连接,不证明实例能访问公网。`unknown` 不能当作离线;关闭网络监控时也可能一直是 `unknown`。没有活动请求时状态可能陈旧,`online` 也不表示每一家供应商都可用。 + +### 与实时事件配合 + +沿用[已认证 Socket.IO 连接](./04-conversations-and-runs.md#submit-conversation),监听 `network:health`,其载荷与上述 HTTP 对象相同。先建立监听,再拉取 HTTP 快照;同一监控实例内仅采用较新的 `seq`,避免晚到的 HTTP 响应覆盖实时事件。新连接不会自动收到一份专用初始事件,重连后应再次补拉。 + +服务重启后序号可能重新开始,不要把旧连接保存的最大序号永久带入新连接。重新建立观察基线,再按新基线比较。这里没有事件回放或从网络状态推导安全重试执行的协议。 + +## 近期工具调用 {#recent-tool-calls} + +### `GET /api/runs/recent-calls` + +接口按服务端解析的可信用户筛选关联 Run,再返回按时间从新到旧排列的概览;不能通过查询参数切换到另一个用户。 + +| 查询参数 | 规则 | +|---|---| +| `limit` | 默认 `30`;单个十进制正整数字符串,转换后为安全整数;没有额外统一条数上限 | + +不提供 `offset`、游标、`total` 或 `hasMore`。空值、负数、小数、重复参数、混杂字符串和非安全整数返回 `400`。 + +```bash +curl --silent --show-error --get \ + --header "X-Agent-Service-Token: ${AGENT_SERVICE_TOKEN}" \ + --data-urlencode 'limit=20' \ + "${BASE_URL}/api/runs/recent-calls" +``` + +```json +{ + "calls": [ + { + "id": "example-tool-invocation", + "timestamp": "2026-10-25T00:00:01.000Z", + "agentId": "your-agent-id", + "runId": "run-example", + "toolName": "Read", + "status": "success", + "durationMs": 12 + } + ] +} +``` + +每项只含上述元数据,不提供工具输入或输出正文。`id` 是不透明调用标识,保留原值,不从中拆出 Run、回执或分页位置;读取 Run 使用独立的 `runId`。 + +`status` 为 `success`、`warning` 或 `error`。有明确工具状态时使用该状态;旧回执缺少该信息时,标签可能来自整轮回执结果。时间也可能来自回执时间,不能据此证明工具副作用已完成、已撤回或某次提交成功。 + +`durationMs` 缺少记录时为 `0`,不代表已经测得零耗时。 + +这是近期概览,不是完整审计导出。读取可能使用有限的 Run 窗口;没有可用索引时只扫描最近 100 个当前用户 Run,不能靠增大 `limit` 穷举全部历史。清除聊天后仍可能保留相关回执中的调用概览。需要具体一轮结果时读取[执行回执](./04-conversations-and-runs.md#list-run-receipts)。 + +## 已记录用量 {#usage-summary} + +### `GET /api/runs/usage/summary` + +返回当前可信用户的聚合信息,不接收 `userId` 身份覆盖参数。 + +| 查询参数 | 规则 | +|---|---| +| `since` | 可选 ISO 时间;按 **Run 开始时间**筛选,不按每条回执时间筛选;无效值当前会被忽略 | +| `agentId` | 可选 Agent ID,按关联 Run 筛选 | +| `timezone` | 可选 IANA 时区,例如 `UTC`、`Asia/Shanghai`;无效值保留 UTC 日期,不返回参数错误 | + +```bash +curl --silent --show-error --get \ + --header "X-Agent-Service-Token: ${AGENT_SERVICE_TOKEN}" \ + --data-urlencode 'since=2026-10-01T00:00:00Z' \ + --data-urlencode "agentId=${AGENT_ID}" \ + --data-urlencode 'timezone=UTC' \ + "${BASE_URL}/api/runs/usage/summary" +``` + +```json +{ + "total": { + "inputTokens": 0, + "outputTokens": 0, + "cacheReadTokens": 0, + "cacheWriteTokens": 0, + "costUsd": 0, + "queryCount": 0 + }, + "byModel": [], + "byDate": [], + "byAgent": [] +} +``` + +上例是没有用量记录时的空汇总。实际 `byModel` 每项包含 `provider`、`model`,`byDate` 包含 `date`,`byAgent` 包含 `agentId`;各项还带与 `total` 相同的六个聚合字段。 + +- `inputTokens` 是非缓存输入,缓存读取和写入分别记在对应字段。 +- `queryCount` 只统计有用量记录的回执查询,不等于 Run 数、全部提交数或工具调用次数。 +- `costUsd` 是已记录的美元成本,不保证等于供应商最终收费;没有用量信息不等于实际没有消耗。 +- `byDate` 以 UTC 日期桶为基础,指定时区时按 UTC 中午映射显示日期,不重新按每条事件归日;不能用于精确本地日结。 +- 有筛选条件或索引不可用时,统计可能只覆盖最近最多 1000 个当前用户 Run。已清除的 Run 仍可能保留用量;结果不是无上限账单。 +- 响应可能缓存约 30 秒。读取不发起模型请求,但可能触发服务端后台索引重建。 + +## 可运行读取示例 {#observe-example} + +下载 **[observe.mjs](pathname:///api-examples/observe.mjs)**,使用 Node.js 22 或更高版本,无需安装 npm 包: + +```bash +export DESIRECORE_URL='https://your-instance.example' +export DESIRECORE_TOKEN='your-existing-owner-token' +export DESIRECORE_AGENT_ID='your-agent-id' + +node observe.mjs input-config +node observe.mjs network +node observe.mjs recent-calls --limit 20 +node observe.mjs usage --agent-id your-agent-id --timezone UTC +``` + +示例先验证握手,再读取一个接口,输出 `{kind, instanceId, data}`。它不轮询、不自动重试,也不根据观察结果发起执行。返回的配置、调用名称和用量可能属于业务隐私,请保管输出文件。示例会提前拒绝格式不正确的时间、时区和数值;服务端的兼容处理仍以上述参数表为准。 + +## 常见错误 + +| 响应 | 处理方式 | +|---|---| +| `401` | 检查实例令牌与当前实例身份,不通过自报用户 ID 绕过 | +| `400`(近期调用) | 检查 `limit` 是否为单个正十进制安全整数 | +| `503 service_not_ready` | 按响应中的 `Retry-After` 等待后重读;不要因此重新提交工作 | +| `500` | 保存接口路径、状态和时间,检查实例日志;避免把令牌或业务正文写入共享日志 | +| `200` 却是 HTML | 地址或代理可能指向网页;重新执行认证握手并确认服务标识 | + +更多认证、来源限制和响应处理见[错误与排查](./03-errors-and-troubleshooting.md)。 diff --git a/docs/07-api/index.md b/docs/07-api/index.md index 0400627..67bb19d 100644 --- a/docs/07-api/index.md +++ b/docs/07-api/index.md @@ -23,6 +23,7 @@ keywords: [API, 集成, Webhook, Agent Service, 接入, 认证] | 触发现有工作流 | 独立 Workflow Service 地址、服务令牌和管理员提供的定义 | [工作流集成](./06-workflow-integration.md) | | 将 HTTP 合同导入工具 | 选择对应服务的规范文件 | [OpenAPI 下载](./07-openapi.md) | | 让 DesireCore 调用你的业务服务 | 通过 MCP、工具或技能连接 | [MCP 服务器连接](../02-user-guide/09-capabilities/03-mcp-integration.md)与[集成能力概览](../02-user-guide/09-capabilities/06-integrations-overview.md) | +| 排查上游网络、观察近期工具调用和已记录用量 | 实例所有者访问权限 | [实例诊断与用量](./10-observability.md) | ## 凭据与结果 diff --git a/i18n/en/docusaurus-plugin-content-docs/current/07-api/01-connection-and-auth.md b/i18n/en/docusaurus-plugin-content-docs/current/07-api/01-connection-and-auth.md index d686ae4..f32656b 100644 --- a/i18n/en/docusaurus-plugin-content-docs/current/07-api/01-connection-and-auth.md +++ b/i18n/en/docusaurus-plugin-content-docs/current/07-api/01-connection-and-auth.md @@ -154,3 +154,5 @@ Browser calls also depend on HTTPS, browser cross-origin rules, and any allowed - [Agent Webhook](./02-agent-webhook.md): trigger events with the target Agent's dedicated secret. - [Errors and Troubleshooting](./03-errors-and-troubleshooting.md): resolve authentication, startup, request, and asynchronous execution issues. + +- [Instance Diagnostics and Usage](./10-observability.md): distinguish instance connectivity, upstream health and recorded usage. diff --git a/i18n/en/docusaurus-plugin-content-docs/current/07-api/03-errors-and-troubleshooting.md b/i18n/en/docusaurus-plugin-content-docs/current/07-api/03-errors-and-troubleshooting.md index 0f91dd6..5ac58e6 100644 --- a/i18n/en/docusaurus-plugin-content-docs/current/07-api/03-errors-and-troubleshooting.md +++ b/i18n/en/docusaurus-plugin-content-docs/current/07-api/03-errors-and-troubleshooting.md @@ -109,3 +109,7 @@ After a conversation timeout, use the example's `--recover` to read admission re HTTP `200` confirms the read, not availability of every tool-result body. Sensitive results or results whose call identity cannot be verified may contain only placeholders with body references omitted. This does not by itself establish execution failure, damaged history, or a need to rerun work. Preserve actual `status` and `isError` facts and follow [history-result read boundaries](./08-session-history-and-messages.md#history-result-privacy); do not infer success or retry automatically from placeholder text. A per-Run message's `offset` is a byte position, whereas the pagination request's `offset` is a source-row position. Advance by the requested window, not the number of returned messages. History can change while being read; see [session history and messages](./08-session-history-and-messages.md). + +## A connected client cannot reach upstream services + +A connected Socket.IO client does not establish that the instance can reach a model provider. Read [upstream network health](./10-observability.md#network-health) and inspect `reason`, `since` and provider scope. `unknown` is not offline, and a historical `online` does not prove the current request succeeded. Recent calls and usage are observations, not instructions to replay work. diff --git a/i18n/en/docusaurus-plugin-content-docs/current/07-api/04-conversations-and-runs.md b/i18n/en/docusaurus-plugin-content-docs/current/07-api/04-conversations-and-runs.md index c5f8a9e..b0d3f88 100644 --- a/i18n/en/docusaurus-plugin-content-docs/current/07-api/04-conversations-and-runs.md +++ b/i18n/en/docusaurus-plugin-content-docs/current/07-api/04-conversations-and-runs.md @@ -12,7 +12,7 @@ Deployment owners can query Agents and execution records in their own standalone ## Read Scope -The Agent list, Run list, Run details, and runtime state are instance data reads for the instance owner. They do not provide per-user isolation between multiple tenants. Receipt reads additionally match the trusted identity resolved by the server to the Run's user ownership; both a missing Run and a Run belonging to another user return `404`. +The Agent list, input configuration, Run list, Run details, and runtime state are instance data reads for the instance owner. They do not provide per-user isolation between multiple tenants. Receipt reads additionally match the trusted identity resolved by the server to the Run's user ownership; both a missing Run and a Run belonging to another user return `404`. Agent IDs, Run IDs, `user_id`, and `conversation_id` identify records; they are not access credentials. Supplying a user ID, knowing a Run ID, or having seen a record elsewhere does not replace authentication and the endpoint's ownership checks. @@ -58,6 +58,50 @@ Example response structure: | `ui`, `avatar`, `avatarImage` | Optional UI and avatar summaries; configured avatar metadata does not establish that its file is usable | | `unread`, `pinned`, `pinnedAt`, `folded`, `groupOverride`, `nestMembers` | Optional unread information and display preferences for the instance's current user; do not fabricate observed values for absent fields | +## Read Agent input configuration {#agent-input-config} + +### `GET /api/agents/:id/input-config` + +:::info Effective version +`v10.0.180+` (reading Agent input configuration). +::: + +After selecting an Agent, read its lightweight input configuration to determine whether it currently uses separate conversations in manual mode. Use the Agent list's `id` for `:id`, containing only letters, digits, hyphens, or underscores. There is no request body or query parameter. Reading does not create a session or send a greeting. + +```bash +curl --silent --show-error \ + --header "X-Agent-Service-Token: ${AGENT_SERVICE_TOKEN}" \ + "${BASE_URL}/api/agents/${AGENT_ID}/input-config" +``` + +```json +{ + "version": 1, + "agentId": "your-agent-id", + "manual": true, + "configRevision": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" +} +``` + +| Field | Meaning | +|---|---| +| `version` | Currently integer `1`; report an unknown version as incompatible rather than continuing in ordinary conversation mode | +| `agentId` | The Agent actually read; verify it matches the selected target | +| `manual` | `true` means separate conversations in manual mode; a valid configuration that omits this mode returns `false` | +| `configRevision` | A 64-character lowercase hexadecimal content digest; compare equality only, without ordering by value or time | + +Responses use `Cache-Control: no-store`. Read again when changing targets or preparing input. `configRevision` neither locks subsequent configuration nor grants execution permission, and is not an increasing event sequence. `manual: false` describes the input mode; execution still requires its own permission and state checks. Keep a failed read unavailable instead of substituting `manual: false`. + +| HTTP / `code` | Action | +|---|---| +| `400 AGENT_INPUT_CONFIG_INVALID_ID` | Use the selected Agent's actual ID | +| `404 AGENT_NOT_FOUND` | The target is unavailable or missing; select again. This business error does not mean an older service lacks the endpoint | +| `409 AGENT_INPUT_CONFIG_CHANGED` | Configuration changed while being read; read again before updating the input UI | +| `503 AGENT_INPUT_CONFIG_INVALID` | Configuration is invalid; ask the administrator to repair it | +| `503 AGENT_INPUT_CONFIG_UNREADABLE` | Configuration is temporarily unreadable; ask the administrator to check it before retrying the read | + +An older service without this route may also return `404`. Check the JSON `code`, handshake, and client version rather than treating every `404` as a safe fallback. See [Errors and Troubleshooting](./03-errors-and-troubleshooting.md) for authentication and service readiness failures, and the [observation example](./10-observability.md#observe-example) for a runnable call. + ## List Runs {#list-runs} ### `GET /api/runs` diff --git a/i18n/en/docusaurus-plugin-content-docs/current/07-api/07-openapi.md b/i18n/en/docusaurus-plugin-content-docs/current/07-api/07-openapi.md index 053a016..1378353 100644 --- a/i18n/en/docusaurus-plugin-content-docs/current/07-api/07-openapi.md +++ b/i18n/en/docusaurus-plugin-content-docs/current/07-api/07-openapi.md @@ -26,6 +26,11 @@ Also available: **[model protocol proxy OpenAPI JSON](pathname:///openapi/compat | `POST /api/agents/{id}/webhooks/secret/rotate` | Rotate the Agent's dedicated Webhook secret | [Agent Webhook](./02-agent-webhook.md) | | `GET /api/agents/{agentId}/schedule-messages` | Read visible background messages and deliveries | [Agent Webhook](./02-agent-webhook.md) | | `GET /api/agents` | Read Agent summaries | [Agent list](./04-conversations-and-runs.md#list-agents) | +| `GET /api/agents/{id}/input-config` | Read lightweight input mode and configuration digest | [Input configuration](./04-conversations-and-runs.md#agent-input-config) | +| `GET /api/network/health` | Read upstream network observations | [Instance Diagnostics and Usage](./10-observability.md#network-health) | +| `GET /api/runs/recent-calls` | Read recent tool-call metadata for the current user | [Recent tool calls](./10-observability.md#recent-tool-calls) | +| `GET /api/runs/usage/summary` | Read recorded usage aggregates for the current user | [Recorded usage](./10-observability.md#usage-summary) | +| `GET /api/sessions/messages/all` | Read merged history with revision pagination | [Merged history](./08-session-history-and-messages.md#merged-history) | | `GET /api/runs` | Read filtered instance Run metadata | [Run list](./04-conversations-and-runs.md#list-runs) | | `GET /api/runs/{id}` | Read one Run's metadata | [Run details](./04-conversations-and-runs.md#get-run) | | `GET /api/runs/{id}/runtime-state` | Read the raw Run state | [Run state](./04-conversations-and-runs.md#get-run-runtime-state) | @@ -100,3 +105,5 @@ A conversation submission's `202` response also does not prove completion. Conne The file's `info.version` is the version of this HTTP interface description. It is separate from DesireCore feature effective versions and the handshake's `serverVersion` and `apiProtocolVersion`. For native media playback, use the complete `/media/{filename}?expires=…&signature=…` URL returned by the issuing endpoint. This read endpoint uses its short-lived signature rather than an instance token. Attachment downloads still require instance authentication. Consult each guide for history, topic, and media access rules. + +Runnable observation and merged-history readers are **[observe.mjs](pathname:///api-examples/observe.mjs)** and **[history.mjs](pathname:///api-examples/history.mjs)**. Restart merged-history reads on revision conflicts instead of combining old pages. Usage aggregates are not final provider invoices. diff --git a/i18n/en/docusaurus-plugin-content-docs/current/07-api/08-session-history-and-messages.md b/i18n/en/docusaurus-plugin-content-docs/current/07-api/08-session-history-and-messages.md index af4199f..84ac929 100644 --- a/i18n/en/docusaurus-plugin-content-docs/current/07-api/08-session-history-and-messages.md +++ b/i18n/en/docusaurus-plugin-content-docs/current/07-api/08-session-history-and-messages.md @@ -12,7 +12,7 @@ Deployment owners can use these endpoints to read their own history on a standal ## Read scope and identifiers {#history-scope} -Session listings, search, and reads of a specific Run use the user identity resolved by the service. Requests for a specific Run check ownership before returning content. A missing, other-user, or cleared Run returns `404`. Knowing its ID does not grant access, and a caller-supplied `userId` cannot select another user's data. +Session listings, merged history, search, and reads of a specific Run use the user identity resolved by the service. Requests for a specific Run check ownership before returning content. A missing, other-user, or cleared Run returns `404`. Knowing its ID does not grant access, and a caller-supplied `userId` cannot select another user's data. The two topic endpoints read Agent conversation memory for the connected instance's current local user profile. They are intended for the instance owner and do not provide tenant isolation for a shared service. A missing profile returns `401`; ask the instance administrator to check it. @@ -165,6 +165,81 @@ While execution is in progress and output has not been persisted, the response m `live` is a replaceable temporary display snapshot. It may roll back or disappear after a terminal event or persistence of the final message. It is excluded from `total` and is not a persisted message, receipt, or success signal. It contains displayable output text and a limited set of tool states; do not use it to reconstruct tool inputs or execution. `text` is limited to 16,384 characters and `toolCalls` to 128 entries. +## Read merged history {#merged-history} + +### `GET /api/sessions/messages/all` + +Use this endpoint to display the continuous visible timeline of an Agent or one conversation. It filters by trusted user ownership before selecting visible fork branches, merging source messages, applying the public projection, and paging. Its offsets differ from per-Run source-row paging. `all` does not mean every execution branch or a complete backup. + +| Query parameter | Meaning | +|---|---| +| `agentId` | Required target Agent ID | +| `conversationId` | Optional exact conversation lineage within that Agent, such as an existing `__manual:…` conversation ID; grants no access to other conversations | +| `preferredRunId` | Optional visible history branch window; neither an exact-Run filter nor an access credential | +| `order` | `desc` by default (newest first), or `asc`; other values fall back to `desc` | +| `limit` | Default `100`, range `0`–`1000`; `0` returns empty `items`, the total, and revision with `hasMore: false`, rather than all results | +| `offset` | Default `0`; a nonnegative safe integer position in the complete merged view | +| `expectedRevision` | Send the first page's `historyRevision` on later pages: `h1:` followed by 64 lowercase hexadecimal characters | + +Send canonical decimal integers for `limit` and `offset`. The service accepts legacy integer-prefix parsing; do not depend on conversions of mixed strings or fractions. The largest safe integer is `9007199254740991`. + +Without `conversationId`, an Agent currently in manual mode excludes separate manual conversations. Once manual mode is disabled, existing manual conversations join the default timeline again. An explicit `conversationId` selects only that conversation. Cleared and archived records, delegated child Runs, and dedicated schedule and Webhook execution timelines are excluded. Mention execution timelines remain separate, but visible Conversation source messages can appear. No visible history returns an empty page; it neither establishes that an Agent is missing nor reveals other users' records. + +```bash +curl --silent --show-error --get \ + --header "X-Agent-Service-Token: ${AGENT_SERVICE_TOKEN}" \ + --data-urlencode "agentId=${AGENT_ID}" \ + --data-urlencode 'order=asc' --data-urlencode 'limit=100' \ + --data-urlencode 'offset=0' \ + "${BASE_URL}/api/sessions/messages/all" +``` + +```json +{ + "items": [ + { + "runId": "run-example", + "messageIndex": 0, + "offset": 0, + "sessionTime": "2026-10-15T08:00:00Z", + "message": { + "id": "d8d53a7a-478e-4d96-970e-657dece814f7", + "role": "user", + "content": "Please review the release notes", + "timestamp": 1792051200000 + } + } + ], + "total": 1, + "hasMore": false, + "historyRevision": "h1:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" +} +``` + +Each `items` entry wraps `runId`, `messageIndex`, `sessionTime`, and a public `message`. Persisted Run messages may also carry a byte `offset`. Positions inside entries are not next-page parameters: advance by **the request's `offset + limit`**, using `hasMore` to decide whether to continue. `total` counts merged public entries, rather than the sum of each Run's source rows. + +**Mention source entries** may carry `message.mentionSourceOnly: true` and a synthetic outer `runId` beginning with `__mention_source:`. They have no actual Run ownership and may have no byte `offset`. Preserve Mention references for display. Do not send synthetic IDs to Run, follow-up, or recovery APIs, or add source entries to model replay history. Prefer `message.id` for linking messages; do not deduplicate all Runs solely by text. + +### Keep one public view revision + +1. Begin at `offset=0` and save the first page's `historyRevision`. +2. Keep the same `agentId`, `conversationId`, `preferredRunId`, `order`, and page size on later pages, sending `expectedRevision`. +3. Buffer the current attempt until all pages finish. On `409 history_revision_changed`, discard every buffered page and start again at `offset=0` for a new revision. Never concatenate pages from different revisions. +4. Retry reads only. History changes, withheld results, and read failures must not trigger conversation submissions, tool reruns, or recovery execution. + +```bash +# HISTORY_REVISION is the first response's complete historyRevision; retain all filters. +curl --silent --show-error --get \ + --header "X-Agent-Service-Token: ${AGENT_SERVICE_TOKEN}" \ + --data-urlencode "agentId=${AGENT_ID}" \ + --data-urlencode 'order=asc' --data-urlencode 'limit=100' \ + --data-urlencode 'offset=100' \ + --data-urlencode "expectedRevision=${HISTORY_REVISION}" \ + "${BASE_URL}/api/sessions/messages/all" +``` + +Content edits, deletions, branch changes, or public projection changes can invalidate the revision. `historyRevision` hashes the current complete public view; it is not a persistent cursor, transaction snapshot, or incremental log. Later reads must confirm the revision again. Each page may read and sort all visible history, so bound page counts and avoid unbounded concurrent scans of large histories. + ## Search an Agent's messages {#search-messages} ### `GET /api/sessions/messages/search` @@ -273,24 +348,30 @@ node history.mjs list --limit 20 --all-pages node history.mjs session run-example node history.mjs messages run-example --limit 100 --all-pages node history.mjs messages delegated-run-id --inspect-run +node history.mjs all --order asc --limit 100 --all-pages +node history.mjs all --conversation-id '__manual:existing-conversation' --limit 0 node history.mjs search --query 'release' --preview-chars 240 --all-pages node history.mjs overview node history.mjs matter mat_example ``` -The example checks the instance handshake before issuing GET requests. It does not submit queries, send follow-ups, or cancel Runs. Each command reads one page by default. `--all-pages` reads up to 20 pages, adjustable with `--max-pages`. Output is newline-delimited JSON containing history content; store it as private data. +The example checks the instance handshake before issuing GET requests. It does not submit queries, send follow-ups, or cancel Runs. Each command reads one page by default. `--all-pages` reads up to 20 pages per attempt, adjustable with `--max-pages`. Output is newline-delimited JSON containing history content; store it as private data. + +`list` accepts `--include-archived`. `search` also supports `--type`, `--preferred-run-id`, and `--preview-chars`. The `messages` inspection view requires the explicit `--inspect-run` option. `all` supports `--conversation-id`, `--order`, and `--preferred-run-id`; for a later single page, pair `--offset` with `--expected-revision`. -`list` accepts `--include-archived`. `search` also supports `--type`, `--preferred-run-id`, and `--preview-chars`. The `messages` inspection view requires the explicit `--inspect-run` option. +Exit code `0` means the requested reads succeeded; `1` indicates a configuration or HTTP failure. Code `2` means pagination reached its configured limit, stopped after observing a changed total or repeated revision conflicts, or received a truncated topic response. A stop record's `nextOffset` provides the next read position, accompanied by `historyRevision` for `all`. A revision-conflict stop provides `restartOffset: 0`; do not resume at an old offset. -Exit code `0` means the requested reads succeeded; `1` indicates a configuration or HTTP failure. Code `2` means pagination reached its configured limit, stopped after observing a changed total, or received a truncated topic response. A stop record's `nextOffset` provides the next read position. Check whether history changed before restarting. +`all --all-pages` buffers the current attempt and discards it on a revision conflict before restarting at the first page. It restarts at most once by default (`--max-restarts` changes this), with `--max-pages` applying to each attempt. Success emits pages from one revision. Reaching the page bound emits a revision-tagged partial window and exits `2`. `all --limit 0` makes only one metadata read. -These endpoints provide no version ensuring consistency across pages. Appending, editing, or clearing history during a scan can produce duplicates or omissions. The example stops if `total` changes, but cannot detect content changes that leave the total unchanged. Do not treat a paginated scan as an audit snapshot from a fixed point in time. +The other paginated endpoints provide no revision ensuring consistency across pages. Appending, editing, or clearing history during a scan can produce duplicates or omissions. The example stops if `total` changes, but cannot detect content changes that leave the total unchanged. Do not treat a paginated scan as an audit snapshot from a fixed point in time. ## Common errors {#history-errors} | HTTP / error | Action | |---|---| -| `400 agentId is required` | Provide an actual Agent ID when searching | +| `400 agentId is required` | Provide an actual Agent ID for merged history or search | +| `400 invalid_history_page` | For merged history, use `limit` from `0` to `1000`, a nonnegative safe integer `offset`, and the complete `expectedRevision` format | +| `409 history_revision_changed` | Discard every page from the attempt and restart at the first page; do not rerun execution | | `400 Invalid history page parameters` | Use nonnegative safe integers for search `limit` and `offset`, and check the `previewChars` range | | `400 Invalid runId` | Use a Run ID returned by the session list, not a path or runtime session ID | | `401` | Check the instance token; for `User not authenticated` or `User not found`, ask the administrator to check the instance's current user profile | diff --git a/i18n/en/docusaurus-plugin-content-docs/current/07-api/10-observability.md b/i18n/en/docusaurus-plugin-content-docs/current/07-api/10-observability.md new file mode 100644 index 0000000..a28bc87 --- /dev/null +++ b/i18n/en/docusaurus-plugin-content-docs/current/07-api/10-observability.md @@ -0,0 +1,176 @@ +--- +title: Instance Diagnostics and Usage +description: Read upstream network health, recent tool calls and recorded usage, with their identity, freshness and aggregation limits. +slug: /api/observability +sidebar_position: 10 +keywords: [API, network health, tool calls, usage, tokens, diagnostics] +--- + +# Instance Diagnostics and Usage + +Deployment owners can read the following observations using their existing instance origin and `X-Agent-Service-Token`. Complete the [connection check](./01-connection-and-auth.md) first, then select the endpoint that answers your question. + +| What you need to observe | Endpoint | What it does not prove | +|---|---|---| +| The instance's latest upstream network observation | `GET /api/network/health` | A particular model, tool or request succeeded | +| Recent tool calls belonging to the current user | `GET /api/runs/recent-calls` | Complete audit history or completion of a conversation turn | +| Recorded tokens and cost for the current user | `GET /api/runs/usage/summary` | A final provider invoice or precise local daily accounting | + +These requests do not submit conversations or replay tools. Their scopes, observation times and aggregation rules differ; combining them does not prove completion of one request. Follow [request reconciliation](./04-conversations-and-runs.md#request-reconciliation) to correlate submissions and terminal events. + +## Upstream network health {#network-health} + +### `GET /api/network/health` + +No request body or query parameters. Success returns the state object directly: + +```bash +curl --silent --show-error \ + --header "X-Agent-Service-Token: ${AGENT_SERVICE_TOKEN}" \ + "${BASE_URL}/api/network/health" +``` + +```json +{ + "status": "unknown", + "reason": "init", + "since": "2026-10-25T00:00:00.000Z", + "seq": 0 +} +``` + +| Field | Meaning | +|---|---| +| `status` | `online`: a recent upstream request or probe succeeded; `degraded`: network failures or some providers are unreachable; `offline`: probes or other evidence confirm unreachability; `unknown`: insufficient samples, such as after startup or wake | +| `reason` | `init`, `request_ok`, `probe_ok`, `request_failed`, `probe_failed`, `stream_stall`, `multi_host_dns`, `suspend_resume` | +| `since` | ISO time when the current `status` began; a reason change within the same status need not update it | +| `seq` | Increasing sequence within the current network monitor, used to compare snapshots and late events; not a persistent version across service restarts | +| `scope` | Optional `global` or `provider`, meaningful for abnormal states | +| `affectedProviders` | Optional provider ID array used with `scope: provider` | +| `subcategory` | Optional `dns`, `tls`, `tcp_reset`, `refused`, `unreachable`, `timeout`, `stream_interrupted` for diagnosis | + +A connected Socket.IO connection only proves connectivity between your client and the instance, not from the instance to the internet. Do not treat `unknown` as offline; disabled monitoring may remain `unknown`. Observations may become stale without active requests, and `online` does not establish that every provider is available. + +### Combining snapshots and live events + +Use the existing [authenticated Socket.IO connection](./04-conversations-and-runs.md#submit-conversation) to listen for `network:health`, whose payload matches the HTTP object. Install the listener before fetching the snapshot; within the same monitor, accept the newer `seq` so a late HTTP response cannot overwrite a live update. A new connection does not receive a dedicated initial event automatically; fetch another snapshot after reconnecting. + +The sequence may restart when the service restarts. Do not permanently carry the old connection's maximum sequence into a new one. Establish a fresh observation baseline before comparing new updates. This is neither event replay nor a protocol for deciding when execution is safe to retry. + +## Recent tool calls {#recent-tool-calls} + +### `GET /api/runs/recent-calls` + +The service filters associated Runs by the trusted user identity before returning a newest-first overview. Query parameters cannot select another user. + +| Query parameter | Rule | +|---|---| +| `limit` | Default `30`; a single positive decimal integer string that converts to a safe integer; no additional common item cap | + +There is no `offset`, cursor, `total` or `hasMore`. Empty, negative, fractional, repeated, mixed-text and unsafe values return `400`. + +```bash +curl --silent --show-error --get \ + --header "X-Agent-Service-Token: ${AGENT_SERVICE_TOKEN}" \ + --data-urlencode 'limit=20' \ + "${BASE_URL}/api/runs/recent-calls" +``` + +```json +{ + "calls": [ + { + "id": "example-tool-invocation", + "timestamp": "2026-10-25T00:00:01.000Z", + "agentId": "your-agent-id", + "runId": "run-example", + "toolName": "Read", + "status": "success", + "durationMs": 12 + } + ] +} +``` + +Each item contains only this metadata, without tool input or output bodies. Preserve `id` as an opaque call identifier. Do not parse it into a Run, receipt or page position; use the separate `runId` to read the Run. + +`status` is `success`, `warning` or `error`. Explicit tool status is used when available; older receipts without it may derive the label from the overall receipt outcome. The time may also come from the receipt. These observations do not prove that a tool's side effects completed, were reverted, or that a submission succeeded. + +A missing `durationMs` is represented as `0`, not a measured zero duration. + +This is a recent overview, not a complete audit export. Reads may use a bounded Run window; without a usable index, only the latest 100 current-user Runs are scanned. Increasing `limit` cannot enumerate all history. Call summaries from retained receipts may remain after clearing chat. Read [execution receipts](./04-conversations-and-runs.md#list-run-receipts) for a particular turn's result. + +## Recorded usage {#usage-summary} + +### `GET /api/runs/usage/summary` + +Returns aggregates for the trusted current user. There is no `userId` identity override parameter. + +| Query parameter | Rule | +|---|---| +| `since` | Optional ISO time filtering by **Run start time**, not each receipt's time; invalid values are currently ignored | +| `agentId` | Optional Agent ID filtering associated Runs | +| `timezone` | Optional IANA time zone, such as `UTC` or `Asia/Shanghai`; invalid values retain UTC dates rather than returning a parameter error | + +```bash +curl --silent --show-error --get \ + --header "X-Agent-Service-Token: ${AGENT_SERVICE_TOKEN}" \ + --data-urlencode 'since=2026-10-01T00:00:00Z' \ + --data-urlencode "agentId=${AGENT_ID}" \ + --data-urlencode 'timezone=UTC' \ + "${BASE_URL}/api/runs/usage/summary" +``` + +```json +{ + "total": { + "inputTokens": 0, + "outputTokens": 0, + "cacheReadTokens": 0, + "cacheWriteTokens": 0, + "costUsd": 0, + "queryCount": 0 + }, + "byModel": [], + "byDate": [], + "byAgent": [] +} +``` + +The example shows an empty aggregate without usage records. Each `byModel` item adds `provider` and `model`, each `byDate` item adds `date`, and each `byAgent` item adds `agentId`. All carry the same six aggregate fields as `total`. + +- `inputTokens` excludes cached input; cache reads and writes have separate fields. +- `queryCount` counts receipt queries with usage records, not Runs, all submissions or tool calls. +- `costUsd` is the recorded dollar cost, not a guarantee of the provider's final charge. Missing usage records do not establish zero consumption. +- `byDate` starts with UTC date buckets. A requested time zone maps each bucket using UTC midday instead of regrouping individual events; it is unsuitable for precise local daily accounting. +- Filtered or fallback reads may cover at most the latest 1000 current-user Runs. Cleared Runs may retain usage. This is not an unlimited invoice history. +- Responses may be cached for about 30 seconds. Reads do not issue model requests, but may trigger background index rebuilding on the service. + +## Runnable read example {#observe-example} + +Download **[observe.mjs](pathname:///api-examples/observe.mjs)** and use Node.js 22 or later. No npm packages are required: + +```bash +export DESIRECORE_URL='https://your-instance.example' +export DESIRECORE_TOKEN='your-existing-owner-token' +export DESIRECORE_AGENT_ID='your-agent-id' + +node observe.mjs input-config +node observe.mjs network +node observe.mjs recent-calls --limit 20 +node observe.mjs usage --agent-id your-agent-id --timezone UTC +``` + +The example verifies the handshake, reads one endpoint and emits `{kind, instanceId, data}`. It does not poll, retry automatically or execute work based on observations. Configuration, tool names and usage can be private business data; keep output files private. The example rejects malformed dates, time zones and numbers before connecting; service compatibility behavior remains as described in the parameter tables above. + +## Common errors + +| Response | Action | +|---|---| +| `401` | Check the instance token and current instance identity; a self-reported user ID cannot bypass authentication | +| `400` (recent calls) | Supply one positive decimal safe integer for `limit` | +| `503 service_not_ready` | Wait according to `Retry-After` and repeat the read; do not resubmit work because of this response | +| `500` | Record the endpoint, status and time, then check instance logs; keep tokens and business bodies out of shared logs | +| `200` containing HTML | The origin or proxy may point to a website; repeat the authenticated handshake and verify the service marker | + +See [Errors and Troubleshooting](./03-errors-and-troubleshooting.md) for authentication, origin restrictions and response handling. diff --git a/i18n/en/docusaurus-plugin-content-docs/current/07-api/index.md b/i18n/en/docusaurus-plugin-content-docs/current/07-api/index.md index 6aee526..e842081 100644 --- a/i18n/en/docusaurus-plugin-content-docs/current/07-api/index.md +++ b/i18n/en/docusaurus-plugin-content-docs/current/07-api/index.md @@ -23,6 +23,7 @@ Connect your scripts or services to DesireCore using the address and credentials | Trigger an existing workflow | Separate Workflow Service address, service token, and administrator-provided definition | [Workflow integration](./06-workflow-integration.md) | | Import HTTP contracts into a tool | Select the spec for your service | [OpenAPI downloads](./07-openapi.md) | | Let DesireCore call your business service | MCP server, tools or skills | [MCP integration](../02-user-guide/09-capabilities/03-mcp-integration.md) and [integration overview](../02-user-guide/09-capabilities/06-integrations-overview.md) | +| Diagnose upstream connectivity and observe recent tool calls and recorded usage | Instance-owner access | [Instance Diagnostics and Usage](./10-observability.md) | ## Credentials and results diff --git a/scripts/check-api-docs.mjs b/scripts/check-api-docs.mjs index c0c432d..aee384e 100644 --- a/scripts/check-api-docs.mjs +++ b/scripts/check-api-docs.mjs @@ -215,7 +215,7 @@ async function checkSpecs(catalog, pages) { } async function checkClientExampleMirror() { - for (const [maintained, published] of [['client.mjs', 'agent-client.mjs'], ['history.mjs', 'history.mjs'], ['attachments.mjs', 'attachments.mjs']]) { + for (const [maintained, published] of [['client.mjs', 'agent-client.mjs'], ['history.mjs', 'history.mjs'], ['attachments.mjs', 'attachments.mjs'], ['observe.mjs', 'observe.mjs']]) { const sourceFile = `api/examples/agent-client/${maintained}` const source = await readBytes(sourceFile) const downloadFile = `static/api-examples/${published}` diff --git a/static/api-examples/history.mjs b/static/api-examples/history.mjs index b1ce2ad..8796cf3 100644 --- a/static/api-examples/history.mjs +++ b/static/api-examples/history.mjs @@ -7,27 +7,34 @@ const HELP = `Usage: node history.mjs list [--agent-id ID] [--include-archived] [--all-pages] node history.mjs session RUN_ID node history.mjs messages RUN_ID [--inspect-run] [--all-pages] + node history.mjs all --agent-id ID [--conversation-id ID] [--order asc|desc] [--all-pages] node history.mjs search --agent-id ID [--query TEXT] [--type all|image|file|tool] [--all-pages] node history.mjs overview --agent-id ID node history.mjs matter MATTER_ID --agent-id ID Environment: DESIRECORE_URL, DESIRECORE_TOKEN, optional DESIRECORE_AGENT_ID. -Pagination: --offset 0; --limit defaults to list=20, messages=100, search=50. -Search alone accepts --limit 0 (all remaining hits), --preferred-run-id ID, -and --preview-chars 1..8000. --all-pages is bounded by --max-pages 20. +Pagination: --offset 0; --limit defaults to list=20, messages/all=100, search=50. +all accepts --limit 0 (metadata only), --preferred-run-id ID, --expected-revision h1:HASH, +and --max-restarts 1. A later offset requires --expected-revision. Revision conflicts +discard buffered pages and restart all-pages reads at 0; no execution is replayed. +Search accepts --limit 0 (all remaining hits), --preferred-run-id ID, and +--preview-chars 1..8000. --all-pages is bounded by --max-pages 20 per attempt. HTTP: --timeout-ms 10000. Remote origins must use HTTPS. Output: one JSON record per page; history may contain private conversation data. Exit: 0 requested reads succeeded; 1 request/configuration failed; 2 an all-pages read stopped early or a matter response was truncated. -These endpoints do not provide a consistent multi-page history snapshot. +Only all checks the public view revision; it is not a persistent snapshot or backup. `; -const PAGED = new Set(['list', 'messages', 'search']); -const NUMERIC = new Set(['limit', 'offset', 'maxPages', 'timeoutMs', 'previewChars']); +const PAGED = new Set(['list', 'messages', 'search', 'all']); +const NUMERIC = new Set(['limit', 'offset', 'maxPages', 'maxRestarts', 'timeoutMs', 'previewChars']); +const REVISION = /^h1:[0-9a-f]{64}$/; const VALUES = new Map([ ['--agent-id', 'agentId'], ['--limit', 'limit'], ['--offset', 'offset'], ['--query', 'query'], ['--type', 'type'], ['--preferred-run-id', 'preferredRunId'], ['--preview-chars', 'previewChars'], ['--max-pages', 'maxPages'], ['--timeout-ms', 'timeoutMs'], + ['--conversation-id', 'conversationId'], ['--order', 'order'], + ['--expected-revision', 'expectedRevision'], ['--max-restarts', 'maxRestarts'], ]); const FLAGS = new Map([ ['--all-pages', 'allPages'], ['--include-archived', 'includeArchived'], ['--inspect-run', 'inspectRun'], @@ -68,10 +75,10 @@ function identifier(value, label) { function optionsFor(input, env) { const options = { ...input }; - if (!['list', 'session', 'messages', 'search', 'overview', 'matter'].includes(options.command)) throw new Error(HELP); + if (!['list', 'session', 'messages', 'all', 'search', 'overview', 'matter'].includes(options.command)) throw new Error(HELP); options.agentId ??= env.DESIRECORE_AGENT_ID; if (options.agentId !== undefined) identifier(options.agentId, 'Agent ID'); - if (['search', 'overview', 'matter'].includes(options.command) && !options.agentId) throw new Error('Set DESIRECORE_AGENT_ID or --agent-id.'); + if (['all', 'search', 'overview', 'matter'].includes(options.command) && !options.agentId) throw new Error('Set DESIRECORE_AGENT_ID or --agent-id.'); if (['session', 'messages'].includes(options.command)) identifier(options.id, 'Run ID'); else if (options.command === 'matter') { if (typeof options.id !== 'string' || !options.id || /[\\/\u0000-\u001f\u007f]/.test(options.id) || options.id.includes('..') || options.id === '.') { @@ -85,11 +92,19 @@ function optionsFor(input, env) { if (!Number.isSafeInteger(options[key]) || options[key] < 0) throw new Error(`Invalid ${key}.`); } } - options.limit ??= options.command === 'list' ? 20 : options.command === 'messages' ? 100 : 50; + options.limit ??= options.command === 'list' ? 20 : ['messages', 'all'].includes(options.command) ? 100 : 50; options.offset ??= 0; options.maxPages ??= 20; + options.maxRestarts ??= 1; options.timeoutMs ??= 10_000; - if ((options.limit === 0 && options.command !== 'search') || !Number.isSafeInteger(options.offset + options.limit)) throw new Error('Use a positive limit and a safe offset; only search permits limit=0.'); + if ((options.limit === 0 && !['search', 'all'].includes(options.command)) || !Number.isSafeInteger(options.offset + options.limit)) throw new Error('Use a positive limit and a safe offset; only search and all permit limit=0.'); + if (options.command === 'all') { + if (options.limit > 1000) throw new Error('all limit must be 0..1000.'); + if (options.expectedRevision !== undefined && !REVISION.test(options.expectedRevision)) throw new Error('Use the returned h1: revision as --expected-revision.'); + if (options.offset > 0 && options.expectedRevision === undefined) throw new Error('A later all page requires --expected-revision from the same read scope.'); + if (options.order !== undefined && !['asc', 'desc'].includes(options.order)) throw new Error('order must be asc or desc.'); + if (options.conversationId !== undefined && (typeof options.conversationId !== 'string' || !options.conversationId || /[\u0000-\u001f\u007f]/.test(options.conversationId))) throw new Error('Use a nonempty conversation ID.'); + } else if (['conversationId', 'order', 'expectedRevision', 'maxRestarts'].some(key => input[key] !== undefined)) throw new Error('Revision and merged-history options apply only to all.'); if (options.maxPages < 1 || options.timeoutMs < 1) throw new Error('maxPages and timeoutMs must be positive.'); if (options.previewChars !== undefined && (options.previewChars < 1 || options.previewChars > 8000)) throw new Error('previewChars must be 1..8000.'); if (options.type !== undefined && !['all', 'image', 'file', 'tool'].includes(options.type)) throw new Error('type must be all, image, file or tool.'); @@ -97,17 +112,24 @@ function optionsFor(input, env) { if (options.includeArchived && options.command !== 'list') throw new Error('--include-archived applies only to list.'); if (options.inspectRun && options.command !== 'messages') throw new Error('--inspect-run applies only to messages.'); if (!PAGED.has(options.command) && (options.allPages || input.limit !== undefined || input.offset !== undefined)) throw new Error('This endpoint is not paginated.'); - if (options.command !== 'search' && ['query', 'type', 'preferredRunId', 'previewChars'].some(key => options[key] !== undefined)) throw new Error('Search filters apply only to search.'); + if (options.command !== 'search' && ['query', 'type', 'previewChars'].some(key => options[key] !== undefined)) throw new Error('Search filters apply only to search.'); + if (!['search', 'all'].includes(options.command) && options.preferredRunId !== undefined) throw new Error('--preferred-run-id applies only to search or all.'); return options; } -function endpoint(options, offset) { +function endpoint(options, offset, expectedRevision) { const query = new URLSearchParams(); let path; if (options.command === 'list') { path = '/api/sessions'; if (options.agentId) query.set('agentId', options.agentId); if (options.includeArchived) query.set('includeArchived', 'true'); + } else if (options.command === 'all') { + path = '/api/sessions/messages/all'; + query.set('agentId', options.agentId); + for (const [key, value] of Object.entries({ conversationId: options.conversationId, order: options.order, preferredRunId: options.preferredRunId, expectedRevision })) { + if (value !== undefined) query.set(key, value); + } } else if (options.command === 'search') { path = '/api/sessions/messages/search'; query.set('agentId', options.agentId); @@ -145,6 +167,7 @@ export async function runHistory(input, env = process.env, output = {}) { const error = new Error(`HTTP ${response.status}: ${typeof data?.error === 'string' ? data.error : 'request rejected'}`); error.status = response.status; error.code = typeof data?.code === 'string' ? data.code : undefined; + error.apiError = typeof data?.error === 'string' ? data.error : undefined; throw error; } return data; @@ -153,6 +176,42 @@ export async function runHistory(input, env = process.env, output = {}) { if (handshake.service !== 'desirecore-agent-service' || handshake.apiProtocolVersion !== '1' || typeof handshake.instanceId !== 'string' || !handshake.instanceId) { throw new Error('Unexpected service identity or API protocol; history was not requested.'); } + if (options.command === 'all') { + let offset = options.offset, revision = options.expectedRevision, restarts = 0, buffered = []; + while (true) { + let data; + try { data = await get(endpoint(options, offset, revision)); } + catch (error) { + if (error.status !== 409 || error.apiError !== 'history_revision_changed' || !options.allPages) throw error; + // Nothing from an invalidated attempt has been emitted. Restart only GET reads. + buffered = []; + if (restarts >= options.maxRestarts) { + emit({ kind: 'stopped', reason: 'history-revision-changed', restartOffset: 0, discarded: true }); + return { exitCode: 2, pages: 0, restarts, restartOffset: 0 }; + } + emit({ kind: 'restarting', reason: 'history-revision-changed', restartOffset: 0, discarded: true }); + restarts++; offset = 0; revision = undefined; + continue; + } + if (!Array.isArray(data?.items) || data.items.length > options.limit || !Number.isSafeInteger(data.total) || data.total < 0 + || typeof data.hasMore !== 'boolean' || !REVISION.test(data.historyRevision) + || (revision !== undefined && revision !== data.historyRevision)) throw new Error('Unexpected merged history page or revision.'); + revision = data.historyRevision; + buffered.push({ kind: 'all', instanceId: handshake.instanceId, offset, limit: options.limit, data }); + const nextOffset = offset + options.limit; + if (!data.hasMore || !options.allPages || options.limit === 0) { + for (const record of buffered) emit(record); + return { exitCode: 0, pages: buffered.length, restarts, ...(data.hasMore && options.limit > 0 && { nextOffset, historyRevision: revision }) }; + } + if (buffered.length >= options.maxPages || !Number.isSafeInteger(nextOffset)) { + // A bounded partial window is explicitly marked and retains its revision for a later read. + for (const record of buffered) emit(record); + emit({ kind: 'stopped', reason: buffered.length >= options.maxPages ? 'max-pages' : 'invalid-next-offset', nextOffset, historyRevision: revision }); + return { exitCode: 2, pages: buffered.length, restarts, nextOffset, historyRevision: revision }; + } + offset = nextOffset; + } + } let offset = options.offset, pages = 0, firstTotal; while (true) { const data = await get(endpoint(options, offset)); diff --git a/static/api-examples/observe.mjs b/static/api-examples/observe.mjs new file mode 100644 index 0000000..3630809 --- /dev/null +++ b/static/api-examples/observe.mjs @@ -0,0 +1,95 @@ +#!/usr/bin/env node +// Standalone observation reads. Node.js 22+, no packages required. +import { resolve } from 'node:path'; +import { pathToFileURL } from 'node:url'; + +const HELP = `Usage: + node observe.mjs input-config --agent-id ID + node observe.mjs network + node observe.mjs recent-calls [--limit 30] + node observe.mjs usage [--since ISO_DATE] [--agent-id ID] [--timezone UTC] +Environment: DESIRECORE_URL, DESIRECORE_TOKEN, optional DESIRECORE_AGENT_ID. +Options: --timeout-ms 10000. Remote origins must use HTTPS. +Output contains private instance metadata. No automatic retry, execution, or billing reconciliation. +`; + +function originFor(value) { + let url; + try { url = new URL(value); } catch { throw new Error('Set DESIRECORE_URL to the instance origin.'); } + if (url.username || url.password || url.pathname !== '/' || url.search || url.hash + || (url.protocol !== 'https:' && !(url.protocol === 'http:' && ['localhost', '127.0.0.1', '[::1]'].includes(url.hostname)))) { + throw new Error('Use an HTTPS origin without credentials, path, query or fragment; HTTP is allowed only on loopback.'); + } + return url.origin; +} + +export function parseObserveArgs(argv) { + if (argv.includes('--help') || argv.includes('-h')) return { help: true }; + const options = { command: argv[0] }; + const flags = { '--agent-id': 'agentId', '--limit': 'limit', '--since': 'since', '--timezone': 'timezone', '--timeout-ms': 'timeoutMs' }; + for (let i = 1; i < argv.length; i++) { + const key = flags[argv[i]]; + if (!key || Object.hasOwn(options, key) || !argv[i + 1] || argv[i + 1].startsWith('--')) throw new Error(HELP); + options[key] = argv[++i]; + } + return options; +} + +export async function runObserve(input, { env = process.env, emit = value => process.stdout.write(`${JSON.stringify(value)}\n`) } = {}) { + if (input.help) { process.stdout.write(HELP); return; } + const { command } = input; + if (!['input-config', 'network', 'recent-calls', 'usage'].includes(command)) throw new Error(HELP); + const origin = originFor(env.DESIRECORE_URL); + if (typeof env.DESIRECORE_TOKEN !== 'string' || !env.DESIRECORE_TOKEN.trim()) throw new Error('Set DESIRECORE_TOKEN.'); + const allowed = new Set(['command', 'help', 'timeoutMs', ...(command === 'input-config' ? ['agentId'] : command === 'usage' ? ['agentId', 'since', 'timezone'] : command === 'recent-calls' ? ['limit'] : [])]); + if (Object.keys(input).some(key => !allowed.has(key))) throw new Error('An option does not apply to this command.'); + const agentId = ['input-config', 'usage'].includes(command) ? input.agentId ?? env.DESIRECORE_AGENT_ID : undefined; + if ((command === 'input-config' && !agentId) || (agentId !== undefined && !/^[A-Za-z0-9_-]+$/.test(agentId))) throw new Error('Use the Agent ID returned by this instance.'); + const integer = (value, label, fallback) => { + if (value === undefined) return fallback; + if (!/^[1-9][0-9]*$/.test(String(value)) || !Number.isSafeInteger(Number(value))) throw new Error(`${label} must be a positive decimal safe integer.`); + return Number(value); + }; + const timeoutMs = integer(input.timeoutMs, 'timeoutMs', 10_000); + const limit = integer(input.limit, 'limit', 30); + if (input.since !== undefined && !/^\d{4}-\d{2}-\d{2}T.*(?:Z|[+-]\d{2}:\d{2})$/.test(input.since)) throw new Error('Use an ISO timestamp with timezone for --since.'); + if (input.since !== undefined && !Number.isFinite(Date.parse(input.since))) throw new Error('Invalid --since timestamp.'); + if (input.timezone !== undefined) { + try { new Intl.DateTimeFormat('en', { timeZone: input.timezone }); } catch { throw new Error('Use a valid IANA --timezone, for example UTC.'); } + } + async function get(path) { + const response = await fetch(origin + path, { + headers: { 'X-Agent-Service-Token': env.DESIRECORE_TOKEN }, + redirect: 'error', signal: AbortSignal.timeout(timeoutMs), + }); + if (!response.headers.get('content-type')?.toLowerCase().includes('application/json')) throw new Error(`HTTP ${response.status}: expected JSON from the instance API.`); + const value = await response.json(); + if (!response.ok) { + const code = typeof value?.code === 'string' && /^[A-Za-z0-9_-]{1,100}$/.test(value.code) ? ` ${value.code}` : ''; + throw new Error(`Observation read rejected: HTTP ${response.status}${code}`); + } + if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error('Expected an API response object.'); + return value; + } + const metadata = await get('/api/client-handshake'); + if (metadata.service !== 'desirecore-agent-service' || metadata.apiProtocolVersion !== '1' || typeof metadata.instanceId !== 'string' || !metadata.instanceId) throw new Error('The handshake did not identify a compatible DesireCore instance.'); + const query = new URLSearchParams(); + let path; + if (command === 'input-config') path = `/api/agents/${encodeURIComponent(agentId)}/input-config`; + else if (command === 'network') path = '/api/network/health'; + else if (command === 'recent-calls') { path = '/api/runs/recent-calls'; query.set('limit', String(limit)); } + else { + path = '/api/runs/usage/summary'; + for (const [key, value] of Object.entries({ since: input.since, agentId, timezone: input.timezone })) if (value !== undefined) query.set(key, value); + } + const data = await get(path + (query.size ? `?${query}` : '')); + if (command === 'input-config' && (data.version !== 1 || data.agentId !== agentId || typeof data.manual !== 'boolean' || !/^[0-9a-f]{64}$/.test(data.configRevision))) throw new Error('Invalid input configuration; do not assume manual=false.'); + if (command === 'network' && (!['online', 'degraded', 'offline', 'unknown'].includes(data.status) || !Number.isFinite(data.seq) || data.seq < 0 || typeof data.since !== 'string' || typeof data.reason !== 'string')) throw new Error('Invalid network observation.'); + if (command === 'recent-calls' && !Array.isArray(data.calls)) throw new Error('Invalid recent calls observation.'); + if (command === 'usage' && (!data.total || ['byModel', 'byDate', 'byAgent'].some(key => !Array.isArray(data[key])))) throw new Error('Invalid usage observation.'); + emit({ kind: command, instanceId: metadata.instanceId, data }); +} + +if (process.argv[1] && import.meta.url === pathToFileURL(resolve(process.argv[1])).href) { + runObserve(parseObserveArgs(process.argv.slice(2))).catch(error => { process.stderr.write(`${error.message}\n`); process.exitCode = 1; }); +} diff --git a/static/openapi/agent-service.json b/static/openapi/agent-service.json index e19b414..05f9b58 100644 --- a/static/openapi/agent-service.json +++ b/static/openapi/agent-service.json @@ -3,7 +3,7 @@ "jsonSchemaDialect": "http://json-schema.org/draft-07/schema#", "info": { "title": "DesireCore Agent Service — public HTTP subset", - "version": "1.1.1", + "version": "1.2.0", "description": "Selected externally callable endpoints for an administrator-provided standalone instance. This document is a subset of the service, and is not an application release declaration. Consult the linked guides for deployment, authorization and asynchronous results." }, "servers": [ @@ -46,6 +46,10 @@ { "name": "Session history", "description": "Owner-scoped historical messages and bounded conversation-matter reads." + }, + { + "name": "Observability", + "description": "Upstream connectivity, user-scoped recent tool calls and recorded usage. Observations do not authorize or replay work." } ], "paths": { @@ -4331,194 +4335,1054 @@ } } } - } - }, - "components": { - "securitySchemes": { - "InstanceToken": { - "type": "apiKey", - "in": "header", - "name": "X-Agent-Service-Token", - "description": "Existing owner credential supplied by the instance administrator." - }, - "WebhookSecret": { - "type": "http", - "scheme": "bearer", - "description": "Dedicated secret for the target Agent Webhook; not a provider API key or JWT." - }, - "SignedMediaSignature": { - "type": "apiKey", - "in": "query", - "name": "signature", - "description": "Five-minute HMAC-SHA-256 capability issued by POST /api/media/access-urls. 43-character canonical base64url signature bound to the filename and required expires query value. Reusable until expiry; no instance header required for this GET. Expired/invalid links return 401. Application authority, configured Origin restrictions, and readiness are checked separately." - } }, - "schemas": { - "ClientHandshake": { - "type": "object", - "required": [ - "service", - "apiProtocolVersion", - "serverVersion", - "instanceId", - "deploymentMode", - "clientCompatibility", - "capabilities", - "authentication", - "serverTime" + "/api/network/health": { + "get": { + "operationId": "getNetworkHealth", + "tags": [ + "Observability" ], - "properties": { - "service": { - "type": "string", - "const": "desirecore-agent-service", - "description": "固定服务标记,用于拒绝非 DesireCore Agent Service 目标。" - }, - "apiProtocolVersion": { - "type": "string", - "const": "1", - "description": "客户端握手和实例连接协议版本;不等同于应用版本。" - }, - "serverVersion": { - "type": "string", - "pattern": "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-(?:0|[1-9]\\d*|\\d*[A-Za-z-][0-9A-Za-z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[A-Za-z-][0-9A-Za-z-]*))*)?(?:\\+[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?$", - "description": "当前 Agent Service 随客户端发布的语义版本号。" - }, - "instanceId": { - "type": "string", - "pattern": "^inst_[A-Za-z0-9_-]{32}$", - "description": "Agent Service 数据空间的稳定不透明身份。由服务端随机生成并持久化,不得由路径、用户、设备或 token 推导。" + "summary": "Read the current upstream network observation", + "description": "Instance-owner observation. No body or query parameters. unknown is not offline; state can be stale while idle or monitoring is disabled. online does not prove every provider or a particular query succeeded. Authenticated Socket.IO network:health uses the same payload. Install a listener before fetching a snapshot; compare seq only within the same monitor and establish a fresh baseline after reconnection or service restart. No event replay.", + "externalDocs": { + "url": "https://docs.desirecore.com/api/observability#network-health" + }, + "security": [ + { + "InstanceToken": [] + } + ], + "parameters": [], + "responses": { + "200": { + "description": "Read the current upstream network observation.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NetworkHealthState" + }, + "example": { + "status": "unknown", + "reason": "init", + "since": "2026-10-25T00:00:00.000Z", + "seq": 0 + } + } + } }, - "deploymentMode": { - "type": "string", - "enum": [ - "embedded", - "owner", - "external" - ], - "description": "Agent Service 运行形态:embedded=Electron 内嵌;owner=standalone owner 模式;external=standalone external 模式。" + "401": { + "description": "Authentication failed or no trusted user identity could be resolved.", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/OperationsError" + }, + { + "$ref": "#/components/schemas/InstanceAuthenticationError" + } + ] + } + } + } }, - "clientCompatibility": { - "type": "object", - "required": [ - "minimumVersion", - "maximumVersion" - ], - "properties": { - "minimumVersion": { - "type": "string", - "pattern": "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-(?:0|[1-9]\\d*|\\d*[A-Za-z-][0-9A-Za-z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[A-Za-z-][0-9A-Za-z-]*))*)?(?:\\+[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?$", - "description": "允许连接本服务的最低 DesireCore 客户端版本(含)。" - }, - "maximumVersion": { - "type": [ - "string", - "null" - ], - "pattern": "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-(?:0|[1-9]\\d*|\\d*[A-Za-z-][0-9A-Za-z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[A-Za-z-][0-9A-Za-z-]*))*)?(?:\\+[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?$", - "description": "允许连接本服务的最高 DesireCore 客户端版本(含);null 表示不设上限。" + "403": { + "description": "A browser Origin is not allowed by this deployment.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OriginRejection" + } } - }, - "additionalProperties": false + } }, - "capabilities": { - "type": "object", - "required": [ - "workflow", - "mediaSignedUrl" - ], - "properties": { - "workflow": { - "type": "boolean", - "description": "当前部署是否具备可用且完整配置的 Workflow Service 能力。" - }, - "pdfRendering": { - "type": "boolean", - "description": "服务端宿主是否提供 HTML 转 PDF 引擎;缺席表示未知,不代表客户端具有打印传输。" - }, - "mediaSignedUrl": { - "type": "boolean", - "description": "当前实例是否可以签发供原生媒体标签使用的短期 HTTPS URL。" - }, - "queryRequestIdEcho": { - "type": "boolean", - "description": "服务端是否回显客户端 query 的 clientRequestId;缺席表示未知,客户端只能按实际事件观察,禁止按版本推断。" - }, - "webhookManagement": { - "type": "boolean", - "description": "是否支持所有者通过 Webhook 管理 API 配置事件、选择现有工具和轮换 secret;缺席或 false 表示该配置入口不可用。" - }, - "localPairing": { - "type": "boolean" - }, - "localAttachSecret": { - "type": "boolean" - }, - "objectStorageCapability": { - "type": "boolean" - }, - "mailCapability": { - "type": "boolean" + "500": { + "description": "An unexpected server failure occurred.", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/OperationsError" + }, + { + "$ref": "#/components/schemas/FrameworkError" + } + ] + } } - }, - "additionalProperties": { - "type": "boolean", - "description": "未来新增的布尔能力位;旧客户端应忽略不认识的能力名。" } }, - "authentication": { - "type": "object", - "required": [ - "kind", - "scopes" - ], - "properties": { - "kind": { - "type": "string", - "enum": [ - "embedded-capability", - "instance-token" - ], - "description": "本次服务形态使用的客户端鉴权类型:embedded-capability 或 standalone instance-token。" - }, - "scopes": { - "type": "array", - "minItems": 1, - "maxItems": 1, - "uniqueItems": true, - "items": { - "type": "string", - "enum": [ - "owner" + "503": { + "description": "Startup is incomplete or instance authentication is not initialized.", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/ServiceStarting" + }, + { + "$ref": "#/components/schemas/InstanceTokenUninitialized" + } ] - }, - "description": "认证凭据授予的实例作用域;第一版固定为 owner。" + } } }, - "additionalProperties": false - }, - "serverTime": { - "type": "string", - "format": "date-time", - "description": "服务端生成响应时的 ISO 8601 时间,用于诊断时钟偏差,不参与身份判断。" + "headers": { + "Retry-After": { + "schema": { + "type": "string" + }, + "description": "Present for service_not_ready; retry the read-only request after this many seconds." + } + } }, - "product": { - "type": "object", - "required": [ - "editionId", - "appId", - "productName", - "buildKind", - "appVersion" - ], - "properties": { - "editionId": { - "type": "string", - "pattern": "^[a-z0-9][a-z0-9-]*$", - "maxLength": 128, - "description": "服务所在应用的发行版 ID;同机接入发现据此标注候选实例属于哪个发行版。" - }, - "appId": { - "type": "string", + "default": { + "description": "Other service or reverse-proxy failures. Preserve the HTTP status, Content-Type and response body; formats are not uniform.", + "content": { + "application/json": { + "schema": {} + }, + "text/plain": { + "schema": { + "type": "string" + } + }, + "text/html": { + "schema": { + "type": "string" + } + } + } + } + } + } + }, + "/api/runs/recent-calls": { + "get": { + "operationId": "listRecentToolCalls", + "tags": [ + "Observability" + ], + "summary": "Read recent tool-call metadata for the trusted user", + "description": "Filters Runs by the trusted user before reading a bounded recent window. Does not return tool inputs or outputs, offset, cursor or total. Preserve opaque call IDs; use the separate runId for Run reads. Explicit tool status/time takes precedence; older receipts may fall back to receipt outcome/time. Missing duration is represented as zero, not a measured zero. A usable upgraded index and receipt fallback share field projection; without one only the latest 100 user Runs are scanned. This is neither complete audit export nor proof of a query terminal.", + "externalDocs": { + "url": "https://docs.desirecore.com/api/observability#recent-tool-calls" + }, + "security": [ + { + "InstanceToken": [] + } + ], + "parameters": [ + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991, + "default": 30 + }, + "description": "Single positive decimal safe integer; no additional route item cap. Empty, repeated, mixed-text, fractional or unsafe values return 400." + } + ], + "responses": { + "200": { + "description": "Read recent tool-call metadata for the trusted user.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OperationsRecentCallsResponse" + }, + "example": { + "calls": [ + { + "id": "example-tool-invocation", + "timestamp": "2026-10-25T00:00:01.000Z", + "agentId": "your-agent-id", + "runId": "run-example", + "toolName": "Read", + "status": "success", + "durationMs": 12 + } + ] + } + } + } + }, + "401": { + "description": "The instance credential is invalid or no trusted user identity could be resolved.", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/OperationsError" + }, + { + "$ref": "#/components/schemas/InstanceAuthenticationError" + } + ] + } + } + } + }, + "403": { + "description": "A browser Origin is not allowed by this deployment.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OriginRejection" + } + } + } + }, + "500": { + "description": "An unexpected server failure occurred.", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/OperationsError" + }, + { + "$ref": "#/components/schemas/FrameworkError" + } + ] + } + } + } + }, + "503": { + "description": "Startup is incomplete or instance authentication is not initialized.", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/ServiceStarting" + }, + { + "$ref": "#/components/schemas/InstanceTokenUninitialized" + } + ] + } + } + }, + "headers": { + "Retry-After": { + "schema": { + "type": "string" + }, + "description": "Present for service_not_ready; retry the read-only request after this many seconds." + } + } + }, + "default": { + "description": "Other service or reverse-proxy failures. Preserve the HTTP status, Content-Type and response body; formats are not uniform.", + "content": { + "application/json": { + "schema": {} + }, + "text/plain": { + "schema": { + "type": "string" + } + }, + "text/html": { + "schema": { + "type": "string" + } + } + } + }, + "400": { + "description": "Invalid limit; use a single positive decimal safe integer.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OperationsError" + } + } + } + } + } + } + }, + "/api/runs/usage/summary": { + "get": { + "operationId": "getRunUsageSummary", + "tags": [ + "Observability" + ], + "summary": "Read recorded usage aggregates for the trusted user", + "description": "Returns total/byModel/byDate/byAgent without conversation bodies. since filters Run start time, not each receipt; invalid since is ignored. Filtered/fallback scans may cover only the latest 1000 owner Runs; cleared Runs may retain usage. Cached for about 30 seconds. queryCount counts receipt queries carrying usage, not all submissions. costUsd is recorded cost, not a final invoice. byDate maps existing UTC date buckets using UTC midday for timezone; it does not regroup individual events into exact local days. Invalid timezone retains UTC. A read can trigger background index rebuilding; it does not issue a model request.", + "externalDocs": { + "url": "https://docs.desirecore.com/api/observability#usage-summary" + }, + "security": [ + { + "InstanceToken": [] + } + ], + "parameters": [ + { + "name": "since", + "in": "query", + "required": false, + "schema": { + "type": "string" + }, + "description": "Recommended ISO timestamp with timezone; filters Run started_at. Invalid values are ignored rather than rejected." + }, + { + "name": "agentId", + "in": "query", + "required": false, + "schema": { + "type": "string" + }, + "description": "Optional associated Agent ID; does not select another user." + }, + { + "name": "timezone", + "in": "query", + "required": false, + "schema": { + "type": "string" + }, + "description": "IANA zone such as UTC. Maps UTC date buckets, not individual events; invalid values retain UTC dates." + } + ], + "responses": { + "200": { + "description": "Read recorded usage aggregates for the trusted user.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OperationsUsageSummaryResponse" + }, + "example": { + "total": { + "inputTokens": 0, + "outputTokens": 0, + "cacheReadTokens": 0, + "cacheWriteTokens": 0, + "costUsd": 0, + "queryCount": 0 + }, + "byModel": [], + "byDate": [], + "byAgent": [] + } + } + } + }, + "401": { + "description": "The instance credential is invalid or no trusted user identity could be resolved.", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/OperationsError" + }, + { + "$ref": "#/components/schemas/InstanceAuthenticationError" + } + ] + } + } + } + }, + "403": { + "description": "A browser Origin is not allowed by this deployment.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OriginRejection" + } + } + } + }, + "500": { + "description": "An unexpected server failure occurred.", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/OperationsError" + }, + { + "$ref": "#/components/schemas/FrameworkError" + } + ] + } + } + } + }, + "503": { + "description": "Startup is incomplete or instance authentication is not initialized.", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/ServiceStarting" + }, + { + "$ref": "#/components/schemas/InstanceTokenUninitialized" + } + ] + } + } + }, + "headers": { + "Retry-After": { + "schema": { + "type": "string" + }, + "description": "Present for service_not_ready; retry the read-only request after this many seconds." + } + } + }, + "default": { + "description": "Other service or reverse-proxy failures. Preserve the HTTP status, Content-Type and response body; formats are not uniform.", + "content": { + "application/json": { + "schema": {} + }, + "text/plain": { + "schema": { + "type": "string" + } + }, + "text/html": { + "schema": { + "type": "string" + } + } + } + } + } + } + }, + "/api/agents/{id}/input-config": { + "get": { + "operationId": "getAgentInputConfig", + "tags": [ + "Read-only" + ], + "summary": "Read lightweight Agent input configuration", + "description": "Available in client v10.0.180+. Instance-owner read; no per-tenant isolation. Reads fresh configuration without creating a session. Verify version=1 and the returned Agent ID. A valid omitted manual mode defaults to false; failures must not be replaced with false. configRevision compares content equality only: it is not execution permission, a lock, a timestamp, or an event sequence.", + "externalDocs": { + "url": "https://docs.desirecore.com/api/conversations-and-runs#agent-input-config" + }, + "security": [ + { + "InstanceToken": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "pattern": "^[A-Za-z0-9_-]+$" + }, + "description": "Selected actual Agent ID." + } + ], + "responses": { + "200": { + "description": "Fresh lightweight input configuration.", + "headers": { + "Cache-Control": { + "schema": { + "type": "string", + "const": "no-store" + }, + "description": "Do not cache this input configuration read." + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AgentInputConfig" + }, + "example": { + "version": 1, + "agentId": "your-agent-id", + "manual": true, + "configRevision": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + } + } + } + }, + "400": { + "description": "Invalid Agent ID.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AgentInputConfigError" + }, + "example": { + "code": "AGENT_INPUT_CONFIG_INVALID_ID", + "error": "Invalid agent ID" + } + } + } + }, + "401": { + "description": "The instance credential is missing or invalid.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InstanceAuthenticationError" + } + } + } + }, + "403": { + "description": "A browser Origin is not allowed by this deployment.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OriginRejection" + } + } + } + }, + "404": { + "description": "Agent unavailable or missing. AGENT_NOT_FOUND is a business result; do not treat it as an older service lacking this route. A route-unavailable 404 is not this DTO.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AgentInputConfigError" + }, + "example": { + "code": "AGENT_NOT_FOUND", + "error": "Agent not found" + } + } + } + }, + "409": { + "description": "Configuration changed during the read; read again before updating the input UI.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AgentInputConfigError" + }, + "example": { + "code": "AGENT_INPUT_CONFIG_CHANGED", + "error": "Agent input config unavailable" + } + } + } + }, + "503": { + "description": "Input configuration is invalid or unreadable, or the service is not ready. Preserve the error code; never substitute manual=false.", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/AgentInputConfigError" + }, + { + "$ref": "#/components/schemas/ServiceStarting" + }, + { + "$ref": "#/components/schemas/InstanceTokenUninitialized" + } + ] + } + } + } + }, + "default": { + "description": "Other service or reverse-proxy failures. Preserve the HTTP status, Content-Type and response body; formats are not uniform.", + "content": { + "application/json": { + "schema": {} + }, + "text/plain": { + "schema": { + "type": "string" + } + }, + "text/html": { + "schema": { + "type": "string" + } + } + } + } + } + } + }, + "/api/sessions/messages/all": { + "get": { + "operationId": "listMergedHistoryMessages", + "tags": [ + "Session history" + ], + "summary": "Read a revision-checked merged public history page", + "description": "Filters trusted user ownership before content reads and selects visible fork branches. Excludes archived, cleared, delegated, schedule/Webhook, and Mention execution timelines; visible Conversation-owned Mention sources may appear. Without conversationId, separate manual conversations join the default timeline only when manual mode is disabled; an explicit conversationId selects that lineage. preferredRunId selects a history window, not an exact-Run filter or permission. items wrap public messages. Synthetic __mention_source: IDs are display-only source references, not Run IDs for execution or recovery. Keep scope/order fixed and send the first historyRevision as expectedRevision on later pages. On 409, discard all pages of the attempt and reread from offset=0 without rerunning execution. The revision hashes the current complete public view; it is not a persistent cursor, transaction snapshot, incremental log, or backup. Each page may read and sort the entire visible history.", + "externalDocs": { + "url": "https://docs.desirecore.com/api/session-history-and-messages#merged-history" + }, + "security": [ + { + "InstanceToken": [] + } + ], + "parameters": [ + { + "name": "agentId", + "in": "query", + "required": true, + "schema": { + "type": "string", + "minLength": 1 + }, + "description": "Required target Agent ID." + }, + { + "name": "conversationId", + "in": "query", + "required": false, + "schema": { + "type": "string" + }, + "description": "Exact conversation lineage within the Agent; does not grant cross-conversation access." + }, + { + "name": "preferredRunId", + "in": "query", + "required": false, + "schema": { + "type": "string" + }, + "description": "Optional visible branch window; not an exact-Run filter or access credential." + }, + { + "name": "order", + "in": "query", + "required": false, + "schema": { + "type": "string", + "enum": [ + "asc", + "desc" + ], + "default": "desc" + }, + "description": "Order by message timestamp. Other wire values fall back to desc." + }, + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "minimum": 0, + "maximum": 1000, + "default": 100 + }, + "description": "0 returns empty items, total and revision with hasMore=false, not all results. Send canonical decimal integers; the route accepts compatibility integer-prefix parsing." + }, + { + "name": "offset", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991, + "default": 0 + }, + "description": "Position in the merged public view, not an item byte offset or source messageIndex. Send a canonical decimal integer." + }, + { + "name": "expectedRevision", + "in": "query", + "required": false, + "schema": { + "type": "string", + "pattern": "^h1:[0-9a-f]{64}$" + }, + "description": "First page historyRevision, required by clients on later pages to detect a changed public view." + } + ], + "responses": { + "200": { + "description": "Merged public history page, including an empty page when no owned visible history matches.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MergedHistoryPage" + }, + "examples": { + "page": { + "value": { + "items": [ + { + "runId": "run-example", + "messageIndex": 0, + "offset": 0, + "sessionTime": "2026-10-15T08:00:00Z", + "message": { + "id": "d8d53a7a-478e-4d96-970e-657dece814f7", + "role": "user", + "content": "Please review the release notes", + "timestamp": 1792051200000 + } + } + ], + "total": 1, + "hasMore": false, + "historyRevision": "h1:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + } + }, + "sourceOnly": { + "summary": "A Conversation source reference, not an actual Run.", + "value": { + "items": [ + { + "runId": "__mention_source:source-example", + "messageIndex": 0, + "sessionTime": "2026-10-15T08:00:00Z", + "message": { + "id": "source-example", + "role": "user", + "content": "Please review the release notes", + "timestamp": 1792051200000, + "mentionSourceOnly": true, + "mentionSource": { + "kind": "conversation_message", + "source_id": "source-example", + "source_revision": 1, + "conversation_id": "your-agent-id" + }, + "mentionOccurrences": [], + "mentionRecipientAgentIds": [ + "another-agent" + ], + "mentionDeliveryIds": [] + } + } + ], + "total": 1, + "hasMore": false, + "historyRevision": "h1:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + } + }, + "metadataOnly": { + "summary": "limit=0 does not return messages.", + "value": { + "items": [], + "total": 42, + "hasMore": false, + "historyRevision": "h1:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + } + } + } + } + } + }, + "400": { + "description": "Missing agentId or invalid merged page/revision parameters.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HistoryError" + }, + "examples": { + "missingAgent": { + "value": { + "error": "agentId is required" + } + }, + "invalidPage": { + "value": { + "error": "invalid_history_page" + } + } + } + } + } + }, + "401": { + "description": "Missing or invalid instance credential, or unavailable user profile.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HistoryError" + } + } + } + }, + "403": { + "description": "Origin or endpoint-specific access restriction.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HistoryError" + } + } + } + }, + "409": { + "description": "The complete public view changed. Discard the attempt and restart at offset=0 with no expectedRevision.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HistoryError" + }, + "example": { + "error": "history_revision_changed" + } + } + } + }, + "503": { + "description": "Service is not ready or its instance credential is unavailable.", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/ServiceStarting" + }, + { + "$ref": "#/components/schemas/InstanceTokenUninitialized" + }, + { + "$ref": "#/components/schemas/FrameworkError" + } + ] + } + } + } + }, + "default": { + "description": "Other service or reverse-proxy failures. Preserve the HTTP status, Content-Type and response body; formats are not uniform.", + "content": { + "application/json": { + "schema": {} + }, + "text/plain": { + "schema": { + "type": "string" + } + }, + "text/html": { + "schema": { + "type": "string" + } + } + } + } + } + } + } + }, + "components": { + "securitySchemes": { + "InstanceToken": { + "type": "apiKey", + "in": "header", + "name": "X-Agent-Service-Token", + "description": "Existing owner credential supplied by the instance administrator." + }, + "WebhookSecret": { + "type": "http", + "scheme": "bearer", + "description": "Dedicated secret for the target Agent Webhook; not a provider API key or JWT." + }, + "SignedMediaSignature": { + "type": "apiKey", + "in": "query", + "name": "signature", + "description": "Five-minute HMAC-SHA-256 capability issued by POST /api/media/access-urls. 43-character canonical base64url signature bound to the filename and required expires query value. Reusable until expiry; no instance header required for this GET. Expired/invalid links return 401. Application authority, configured Origin restrictions, and readiness are checked separately." + } + }, + "schemas": { + "ClientHandshake": { + "type": "object", + "required": [ + "service", + "apiProtocolVersion", + "serverVersion", + "instanceId", + "deploymentMode", + "clientCompatibility", + "capabilities", + "authentication", + "serverTime" + ], + "properties": { + "service": { + "type": "string", + "const": "desirecore-agent-service", + "description": "固定服务标记,用于拒绝非 DesireCore Agent Service 目标。" + }, + "apiProtocolVersion": { + "type": "string", + "const": "1", + "description": "客户端握手和实例连接协议版本;不等同于应用版本。" + }, + "serverVersion": { + "type": "string", + "pattern": "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-(?:0|[1-9]\\d*|\\d*[A-Za-z-][0-9A-Za-z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[A-Za-z-][0-9A-Za-z-]*))*)?(?:\\+[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?$", + "description": "当前 Agent Service 随客户端发布的语义版本号。" + }, + "instanceId": { + "type": "string", + "pattern": "^inst_[A-Za-z0-9_-]{32}$", + "description": "Agent Service 数据空间的稳定不透明身份。由服务端随机生成并持久化,不得由路径、用户、设备或 token 推导。" + }, + "deploymentMode": { + "type": "string", + "enum": [ + "embedded", + "owner", + "external" + ], + "description": "Agent Service 运行形态:embedded=Electron 内嵌;owner=standalone owner 模式;external=standalone external 模式。" + }, + "clientCompatibility": { + "type": "object", + "required": [ + "minimumVersion", + "maximumVersion" + ], + "properties": { + "minimumVersion": { + "type": "string", + "pattern": "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-(?:0|[1-9]\\d*|\\d*[A-Za-z-][0-9A-Za-z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[A-Za-z-][0-9A-Za-z-]*))*)?(?:\\+[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?$", + "description": "允许连接本服务的最低 DesireCore 客户端版本(含)。" + }, + "maximumVersion": { + "type": [ + "string", + "null" + ], + "pattern": "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-(?:0|[1-9]\\d*|\\d*[A-Za-z-][0-9A-Za-z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[A-Za-z-][0-9A-Za-z-]*))*)?(?:\\+[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?$", + "description": "允许连接本服务的最高 DesireCore 客户端版本(含);null 表示不设上限。" + } + }, + "additionalProperties": false + }, + "capabilities": { + "type": "object", + "required": [ + "workflow", + "mediaSignedUrl" + ], + "properties": { + "workflow": { + "type": "boolean", + "description": "当前部署是否具备可用且完整配置的 Workflow Service 能力。" + }, + "pdfRendering": { + "type": "boolean", + "description": "服务端宿主是否提供 HTML 转 PDF 引擎;缺席表示未知,不代表客户端具有打印传输。" + }, + "mediaSignedUrl": { + "type": "boolean", + "description": "当前实例是否可以签发供原生媒体标签使用的短期 HTTPS URL。" + }, + "queryRequestIdEcho": { + "type": "boolean", + "description": "服务端是否回显客户端 query 的 clientRequestId;缺席表示未知,客户端只能按实际事件观察,禁止按版本推断。" + }, + "webhookManagement": { + "type": "boolean", + "description": "是否支持所有者通过 Webhook 管理 API 配置事件、选择现有工具和轮换 secret;缺席或 false 表示该配置入口不可用。" + }, + "localPairing": { + "type": "boolean" + }, + "localAttachSecret": { + "type": "boolean" + }, + "objectStorageCapability": { + "type": "boolean" + }, + "mailCapability": { + "type": "boolean" + } + }, + "additionalProperties": { + "type": "boolean", + "description": "未来新增的布尔能力位;旧客户端应忽略不认识的能力名。" + } + }, + "authentication": { + "type": "object", + "required": [ + "kind", + "scopes" + ], + "properties": { + "kind": { + "type": "string", + "enum": [ + "embedded-capability", + "instance-token" + ], + "description": "本次服务形态使用的客户端鉴权类型:embedded-capability 或 standalone instance-token。" + }, + "scopes": { + "type": "array", + "minItems": 1, + "maxItems": 1, + "uniqueItems": true, + "items": { + "type": "string", + "enum": [ + "owner" + ] + }, + "description": "认证凭据授予的实例作用域;第一版固定为 owner。" + } + }, + "additionalProperties": false + }, + "serverTime": { + "type": "string", + "format": "date-time", + "description": "服务端生成响应时的 ISO 8601 时间,用于诊断时钟偏差,不参与身份判断。" + }, + "product": { + "type": "object", + "required": [ + "editionId", + "appId", + "productName", + "buildKind", + "appVersion" + ], + "properties": { + "editionId": { + "type": "string", + "pattern": "^[a-z0-9][a-z0-9-]*$", + "maxLength": 128, + "description": "服务所在应用的发行版 ID;同机接入发现据此标注候选实例属于哪个发行版。" + }, + "appId": { + "type": "string", "minLength": 1, "maxLength": 256, "description": "服务所在应用的 appId(macOS bundle id、Windows AUMID 前缀)。" @@ -8591,6 +9455,38 @@ "file": {}, "thinking": { "type": "string" + }, + "mentionSourceOnly": { + "type": "boolean", + "description": "true marks a Conversation source without current-Agent Run ownership. Display only; do not feed into model replay." + }, + "mentionSource": { + "type": "object", + "description": "Conversation-owned Mention source reference, when present.", + "additionalProperties": true + }, + "mentionOccurrences": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": true + }, + "description": "Structured Mention occurrences, when present." + }, + "mentionRecipientAgentIds": { + "type": "array", + "items": { + "type": "string" + }, + "uniqueItems": true + }, + "mentionDeliveryIds": { + "type": "array", + "items": { + "type": "string" + }, + "uniqueItems": true, + "description": "Delivery references, not proof of successful execution." } }, "additionalProperties": true, @@ -8873,133 +9769,618 @@ "HistorySearchPage": { "type": "object", "properties": { - "items": { + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/HistorySearchHit" + } + }, + "total": { + "type": "integer", + "minimum": 0 + }, + "hasMore": { + "type": "boolean" + } + }, + "required": [ + "items", + "total", + "hasMore" + ], + "additionalProperties": true + }, + "ConversationMatterOverview": { + "type": "object", + "properties": { + "agentId": { + "type": "string" + }, + "activeMatters": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "title": { + "type": "string" + }, + "type": { + "type": "string" + } + }, + "required": [ + "id", + "title", + "type" + ], + "additionalProperties": true + } + }, + "recentMatters": { + "type": "array", + "maxItems": 10, + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "title": { + "type": "string" + }, + "type": { + "type": "string" + }, + "completedAt": { + "type": "string" + }, + "summary": { + "type": "string" + } + }, + "required": [ + "id", + "title", + "type" + ], + "additionalProperties": true + } + }, + "totalMatters": { + "type": "integer", + "minimum": 0 + }, + "totalArchived": { + "type": "integer", + "minimum": 0 + } + }, + "required": [ + "agentId", + "activeMatters", + "recentMatters", + "totalMatters", + "totalArchived" + ], + "additionalProperties": true + }, + "ConversationMatterMessages": { + "type": "object", + "properties": { + "matterId": { + "type": "string" + }, + "messages": { + "type": "array", + "maxItems": 120, + "items": {}, + "description": "Public source entries; not guaranteed to have the per-Run SessionMessage shape. Sensitive or unverifiable tool-result bodies may be replaced and external result references removed." + }, + "truncated": { + "type": "boolean", + "const": true, + "description": "Present and true when the source read exceeded 120 messages. Public projection may return fewer entries." + } + }, + "required": [ + "matterId", + "messages" + ], + "additionalProperties": true + }, + "NetworkHealthState": { + "type": "object", + "required": [ + "status", + "reason", + "since", + "seq" + ], + "properties": { + "status": { + "type": "string", + "enum": [ + "online", + "degraded", + "offline", + "unknown" + ], + "description": "网络健康状态:online=上游请求或探测成功;degraded=出现网络类失败但未确认公网断开(或仅个别 Provider 不可达);offline=通用连通性探测失败且近期失败的上游也探测不通,或多 host DNS 失败,确认网络不可达;unknown=启动后尚无样本(或休眠唤醒后待重新确认),UI 不应据此显示异常" + }, + "scope": { + "type": "string", + "enum": [ + "global", + "provider" + ], + "description": "异常范围(仅 degraded/offline 时有意义):global=公网整体不可达;provider=公网通但个别模型服务商不可达(通用探测成功、Provider origin 探测失败)" + }, + "affectedProviders": { + "type": "array", + "items": { + "type": "string" + }, + "description": "scope=provider 时受影响的 Provider ID 列表(供前端展示\"服务商暂不可达\")" + }, + "reason": { + "type": "string", + "enum": [ + "init", + "request_ok", + "probe_ok", + "request_failed", + "probe_failed", + "stream_stall", + "multi_host_dns", + "suspend_resume" + ], + "description": "进入当前状态的原因:init=启动初始;request_ok/probe_ok=真实请求或探测成功;request_failed=上游请求网络类失败;probe_failed=通用连通性探测失败;stream_stall=流式空闲超时;multi_host_dns=窗口期内多个不同 host DNS 失败(最强离线证据);suspend_resume=系统唤醒后重置待确认" + }, + "subcategory": { + "type": "string", + "enum": [ + "dns", + "tls", + "tcp_reset", + "refused", + "unreachable", + "timeout", + "stream_interrupted" + ], + "description": "触发本次状态变化的网络错误子类(若有),供前端给出针对性提示(如 dns → 检查 DNS/代理设置)" + }, + "since": { + "type": "string", + "description": "进入当前状态的 ISO 8601 时间;消费方据此判断状态新鲜度(无活跃请求时状态允许 stale)" + }, + "seq": { + "type": "number", + "minimum": 0, + "description": "单调递增序号;前端用于拒绝乱序/迟到的旧状态事件(快照与实时事件以 seq 大者为准)" + } + }, + "additionalProperties": false + }, + "OperationsRecentCallsResponse": { + "type": "object", + "required": [ + "calls" + ], + "properties": { + "calls": { "type": "array", "items": { - "$ref": "#/components/schemas/HistorySearchHit" - } - }, - "total": { - "type": "integer", - "minimum": 0 - }, - "hasMore": { - "type": "boolean" + "type": "object", + "required": [ + "id", + "timestamp", + "agentId", + "runId", + "toolName", + "status", + "durationMs" + ], + "properties": { + "id": { + "type": "string", + "minLength": 1, + "description": "不透明的稳定调用 ID;优先 invocation_id,旧记录由 Run/Receipt 身份和工具序号确定。不得解析为 Run ID,也不是恢复或授权凭据。" + }, + "timestamp": { + "type": "string", + "format": "date-time", + "description": "优先工具 started_at;旧记录缺失时回退 Receipt started_at,再回退 Run started_at。不是保证精确的工具开始时间。" + }, + "agentId": { + "type": "string", + "minLength": 1, + "description": "执行 Agent ID。" + }, + "runId": { + "type": "string", + "minLength": 1, + "description": "关联 Run ID;详情跳转不得从展示 ID 猜测。" + }, + "toolName": { + "type": "string", + "minLength": 1, + "description": "工具名称。" + }, + "status": { + "type": "string", + "enum": [ + "success", + "warning", + "error" + ], + "description": "优先工具 success/error;旧记录缺失时按 Receipt outcome 的 failed→error、partial→warning、其他→success 投影,不代表 Query 或 Run 终态。" + }, + "durationMs": { + "type": "number", + "minimum": 0, + "description": "已记录工具耗时(毫秒);旧记录缺失时为 0,不表示真实零耗时。" + } + }, + "additionalProperties": false + }, + "description": "当前可信用户最近的工具调用,按时间倒序。" } }, + "additionalProperties": false, + "description": "运行中心活动分区的用户隔离工具调用列表。" + }, + "OperationsUsageSummaryResponse": { + "type": "object", "required": [ - "items", "total", - "hasMore" + "byModel", + "byDate", + "byAgent" ], - "additionalProperties": true - }, - "ConversationMatterOverview": { - "type": "object", "properties": { - "agentId": { - "type": "string" + "total": { + "type": "object", + "required": [ + "inputTokens", + "outputTokens", + "cacheReadTokens", + "cacheWriteTokens", + "costUsd", + "queryCount" + ], + "properties": { + "inputTokens": { + "type": "number", + "minimum": 0, + "description": "非缓存输入 Token 总数。" + }, + "outputTokens": { + "type": "number", + "minimum": 0, + "description": "输出 Token 总数。" + }, + "cacheReadTokens": { + "type": "number", + "minimum": 0, + "description": "缓存读取 Token 总数。" + }, + "cacheWriteTokens": { + "type": "number", + "minimum": 0, + "description": "缓存写入 Token 总数。" + }, + "costUsd": { + "type": "number", + "minimum": 0, + "description": "已记录的美元成本总额。" + }, + "queryCount": { + "type": "number", + "minimum": 0, + "description": "带用量数据的查询次数。" + } + }, + "additionalProperties": false, + "description": "当前可信用户的用量汇总。" }, - "activeMatters": { + "byModel": { "type": "array", "items": { "type": "object", + "required": [ + "inputTokens", + "outputTokens", + "cacheReadTokens", + "cacheWriteTokens", + "costUsd", + "queryCount", + "provider", + "model" + ], "properties": { - "id": { - "type": "string" + "provider": { + "type": "string", + "description": "模型 Provider 标识。" }, - "title": { - "type": "string" + "model": { + "type": "string", + "description": "模型 ID。" }, - "type": { - "type": "string" + "inputTokens": { + "type": "number", + "minimum": 0, + "description": "非缓存输入 Token 总数。" + }, + "outputTokens": { + "type": "number", + "minimum": 0, + "description": "输出 Token 总数。" + }, + "cacheReadTokens": { + "type": "number", + "minimum": 0, + "description": "缓存读取 Token 总数。" + }, + "cacheWriteTokens": { + "type": "number", + "minimum": 0, + "description": "缓存写入 Token 总数。" + }, + "costUsd": { + "type": "number", + "minimum": 0, + "description": "已记录的美元成本总额。" + }, + "queryCount": { + "type": "number", + "minimum": 0, + "description": "带用量数据的查询次数。" } }, - "required": [ - "id", - "title", - "type" - ], - "additionalProperties": true - } + "additionalProperties": false + }, + "description": "按 Provider/模型聚合的用量。" }, - "recentMatters": { + "byDate": { "type": "array", - "maxItems": 10, "items": { "type": "object", + "required": [ + "inputTokens", + "outputTokens", + "cacheReadTokens", + "cacheWriteTokens", + "costUsd", + "queryCount", + "date" + ], "properties": { - "id": { - "type": "string" + "date": { + "type": "string", + "pattern": "^\\d{4}-\\d{2}-\\d{2}$", + "description": "聚合日期(YYYY-MM-DD);UTC 日桶可按其中点映射到请求时区,不是逐事件的精确本地日结。" }, - "title": { - "type": "string" + "inputTokens": { + "type": "number", + "minimum": 0, + "description": "非缓存输入 Token 总数。" }, - "type": { - "type": "string" + "outputTokens": { + "type": "number", + "minimum": 0, + "description": "输出 Token 总数。" }, - "completedAt": { - "type": "string" + "cacheReadTokens": { + "type": "number", + "minimum": 0, + "description": "缓存读取 Token 总数。" }, - "summary": { - "type": "string" + "cacheWriteTokens": { + "type": "number", + "minimum": 0, + "description": "缓存写入 Token 总数。" + }, + "costUsd": { + "type": "number", + "minimum": 0, + "description": "已记录的美元成本总额。" + }, + "queryCount": { + "type": "number", + "minimum": 0, + "description": "带用量数据的查询次数。" } }, + "additionalProperties": false + }, + "description": "按 UTC 日桶聚合的用量;有效 timezone 将桶的 UTC 12:00 映射到本地日期,无法恢复桶内每条记录的本地日界。" + }, + "byAgent": { + "type": "array", + "items": { + "type": "object", "required": [ - "id", - "title", - "type" + "inputTokens", + "outputTokens", + "cacheReadTokens", + "cacheWriteTokens", + "costUsd", + "queryCount", + "agentId" ], - "additionalProperties": true - } - }, - "totalMatters": { - "type": "integer", - "minimum": 0 - }, - "totalArchived": { - "type": "integer", - "minimum": 0 + "properties": { + "agentId": { + "type": "string", + "minLength": 1, + "description": "关联 Agent ID。" + }, + "inputTokens": { + "type": "number", + "minimum": 0, + "description": "非缓存输入 Token 总数。" + }, + "outputTokens": { + "type": "number", + "minimum": 0, + "description": "输出 Token 总数。" + }, + "cacheReadTokens": { + "type": "number", + "minimum": 0, + "description": "缓存读取 Token 总数。" + }, + "cacheWriteTokens": { + "type": "number", + "minimum": 0, + "description": "缓存写入 Token 总数。" + }, + "costUsd": { + "type": "number", + "minimum": 0, + "description": "已记录的美元成本总额。" + }, + "queryCount": { + "type": "number", + "minimum": 0, + "description": "带用量数据的查询次数。" + } + }, + "additionalProperties": false + }, + "description": "按 Agent 聚合的用量。" } }, + "additionalProperties": false, + "description": "运行中心活动分区的用户隔离用量摘要。" + }, + "AgentInputConfig": { + "type": "object", + "additionalProperties": false, "required": [ + "version", "agentId", - "activeMatters", - "recentMatters", - "totalMatters", - "totalArchived" + "manual", + "configRevision" ], - "additionalProperties": true + "properties": { + "version": { + "type": "integer", + "const": 1, + "description": "Projection protocol version; unknown versions do not unlock ordinary input." + }, + "agentId": { + "type": "string", + "pattern": "^[A-Za-z0-9_-]+$", + "description": "Actual Agent read; match it against the selected target." + }, + "manual": { + "type": "boolean", + "description": "Current separate-conversation manual mode. Valid omission in Agent configuration defaults to false." + }, + "configRevision": { + "type": "string", + "pattern": "^[a-f0-9]{64}$", + "description": "Opaque digest of configuration content. Equality only; not a permission, lock, timestamp or sequence." + } + } }, - "ConversationMatterMessages": { + "AgentInputConfigError": { "type": "object", + "required": [ + "code", + "error" + ], "properties": { - "matterId": { + "code": { + "type": "string", + "enum": [ + "AGENT_INPUT_CONFIG_INVALID_ID", + "AGENT_NOT_FOUND", + "AGENT_INPUT_CONFIG_CHANGED", + "AGENT_INPUT_CONFIG_INVALID", + "AGENT_INPUT_CONFIG_UNREADABLE" + ] + }, + "error": { "type": "string" + } + }, + "additionalProperties": true + }, + "MergedHistoryItem": { + "type": "object", + "additionalProperties": false, + "required": [ + "runId", + "messageIndex", + "sessionTime", + "message" + ], + "properties": { + "runId": { + "type": "string", + "description": "Owned source Run ID, or __mention_source:SOURCE_ID for a Conversation source-only entry. Synthetic IDs cannot be used with Run, follow-up or recovery APIs." }, - "messages": { - "type": "array", - "maxItems": 120, - "items": {}, - "description": "Public source entries; not guaranteed to have the per-Run SessionMessage shape. Sensitive or unverifiable tool-result bodies may be replaced and external result references removed." + "messageIndex": { + "type": "number", + "description": "Source row navigation position; source-only entries use 0. Not the merged page offset." }, - "truncated": { - "type": "boolean", - "const": true, - "description": "Present and true when the source read exceeded 120 messages. Public projection may return fewer entries." + "offset": { + "type": "number", + "minimum": 0, + "description": "Optional source byte position for persisted Run messages. Not a page offset." + }, + "sessionTime": { + "type": "string", + "description": "Source session creation time or source creation time, in ISO 8601 form." + }, + "message": { + "$ref": "#/components/schemas/HistoryMessage" } - }, + } + }, + "MergedHistoryPage": { + "type": "object", + "additionalProperties": false, "required": [ - "matterId", - "messages" + "items", + "total", + "hasMore", + "historyRevision" ], - "additionalProperties": true + "properties": { + "items": { + "type": "array", + "maxItems": 1000, + "items": { + "$ref": "#/components/schemas/MergedHistoryItem" + }, + "description": "Public display projection; sensitive result bodies and references may be withheld." + }, + "total": { + "type": "integer", + "minimum": 0, + "description": "Total entries in this complete merged public view, not source rows across Runs." + }, + "hasMore": { + "type": "boolean", + "description": "Whether more entries remain for a positive limit. Always false for limit=0." + }, + "historyRevision": { + "type": "string", + "pattern": "^h1:[0-9a-f]{64}$", + "description": "Current complete public view digest including read scope and order. Later pages send expectedRevision; changes yield 409. Not a persistent cursor or transaction snapshot." + } + } } } }, "x-service": "agent-service", "x-source": { "repository": "desirecore/desirecore", - "revision": "a101a40c14cc9d478c7f9a23d8a125ac2404509b" + "revision": "a0186f6feeaf66eb88787e8b80b8fea5559b8761" } } diff --git a/tsconfig.json b/tsconfig.json index e10da83..7b10be1 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,5 +4,5 @@ "compilerOptions": { "baseUrl": "." }, - "exclude": [".docusaurus", "build", "api/examples/agent-client/test/source-route.fixture.ts", "api/examples/agent-client/test/source-history.fixture.ts"] + "exclude": [".docusaurus", "build", "api/examples/agent-client/test/source-route.fixture.ts", "api/examples/agent-client/test/source-history.fixture.ts", "api/examples/agent-client/test/source-observe.fixture.ts"] }