Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion api/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 与审批模型保留,不把配置文件宣称为不可修改的安全边界。

Expand Down Expand Up @@ -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游标说明仍需修复。该专项结果不扩写为跨用户泄漏或真实数据事故;见验证记录的范围说明。此前生产/搜索验收保留原始批次,不代表本批已部署。
336 changes: 278 additions & 58 deletions api/endpoint-catalog.json

Large diffs are not rendered by default.

17 changes: 16 additions & 1 deletion api/examples/agent-client/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand All @@ -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`.
83 changes: 71 additions & 12 deletions api/examples/agent-client/history.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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'],
Expand Down Expand Up @@ -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 === '.') {
Expand All @@ -85,29 +92,44 @@ 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.');
if (options.preferredRunId !== undefined) identifier(options.preferredRunId, 'preferred Run ID');
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);
Expand Down Expand Up @@ -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;
Expand All @@ -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));
Expand Down
Loading
Loading