feat(contracts): 客户端与服务端协议 v1 及事件模型(A1.7)
JSON Schema 为规范本体(envelope / events / receipt / capability / session / approval / error),TS 镜像在 contracts/events.ts 与 contracts/protocol.ts,人读版本在 docs/spec/protocol.md。11 类事件(message.delta、message.completed、tool.call、tool.result、execution.updated、approval.requested、approval.resolved、run.state、session.updated、error、heartbeat)带会话内单调 seq,支持 Last-Event-ID 断点续传;HTTP 端点覆盖健康检查、身份、项目、会话、发消息、取消、能力清单、执行状态与审批响应。漂移检查卡三处一致:schema、TS、文档;样例共 11 条事件 + 5 条 HTTP。
This commit is contained in:
parent
433684fd79
commit
2023e5312b
|
|
@ -0,0 +1,115 @@
|
|||
/**
|
||||
* 协议 v1 的事件模型(JSON Schema 为准,这里是 TS 镜像)。
|
||||
*
|
||||
* 界面只依赖这一套事件:引擎换 Pi 还是 Codex,事件不变(架构设计 5.2)。
|
||||
* 改事件必须同时改:contracts/schemas/events.schema.json、本文件、docs/spec/protocol.md。
|
||||
*/
|
||||
import type { ExecutionReceipt } from './protocol.js';
|
||||
|
||||
export const PROTOCOL_VERSION = 1;
|
||||
|
||||
/** 所有事件共有的信封字段 */
|
||||
export interface EventEnvelope<TType extends string, TData> {
|
||||
/** 协议版本;改协议就加版本,不改这一版 */
|
||||
v: typeof PROTOCOL_VERSION;
|
||||
/** 会话内单调递增:断点续传与去重靠它 */
|
||||
seq: number;
|
||||
/** 整数毫秒(ADR-0007) */
|
||||
ts: number;
|
||||
sessionId: string;
|
||||
/** 一次运行(回合)的标识 */
|
||||
runId?: string;
|
||||
type: TType;
|
||||
data: TData;
|
||||
}
|
||||
|
||||
export type ExecutionStatus = ExecutionReceipt['status'];
|
||||
|
||||
export interface MessageDeltaData {
|
||||
messageId: string;
|
||||
text: string;
|
||||
}
|
||||
|
||||
export interface MessageCompletedData {
|
||||
messageId: string;
|
||||
finishReason?: 'stop' | 'cancelled' | 'error' | 'length';
|
||||
}
|
||||
|
||||
export interface ToolCallData {
|
||||
callId: string;
|
||||
capabilityId: string;
|
||||
/** 参数摘要,不是参数明文 */
|
||||
argsDigest: string;
|
||||
}
|
||||
|
||||
export interface ToolResultData {
|
||||
callId: string;
|
||||
executionId: string;
|
||||
status: ExecutionStatus;
|
||||
}
|
||||
|
||||
export interface ExecutionUpdatedData {
|
||||
executionId: string;
|
||||
status: ExecutionStatus;
|
||||
errorKind?: string;
|
||||
}
|
||||
|
||||
export interface ApprovalRequestedData {
|
||||
approvalId: string;
|
||||
capabilityId: string;
|
||||
paramsDigest: string;
|
||||
/** 确认卡正文:影响范围与关键参数摘要 */
|
||||
impactSummary: string;
|
||||
/** 目标数据版本;版本变了确认就失效 */
|
||||
revision: number;
|
||||
expiresAt: number;
|
||||
}
|
||||
|
||||
export interface ApprovalResolvedData {
|
||||
approvalId: string;
|
||||
decision: 'approved' | 'rejected' | 'expired';
|
||||
}
|
||||
|
||||
export interface RunStateData {
|
||||
state: 'idle' | 'running' | 'cancelling' | 'cancelled';
|
||||
}
|
||||
|
||||
export interface SessionUpdatedData {
|
||||
title?: string;
|
||||
status?: 'active' | 'archived';
|
||||
}
|
||||
|
||||
export interface ErrorData {
|
||||
code: string;
|
||||
message: string;
|
||||
retryable: boolean;
|
||||
errorKind?: string;
|
||||
}
|
||||
|
||||
export type MessageDeltaEvent = EventEnvelope<'message.delta', MessageDeltaData>;
|
||||
export type MessageCompletedEvent = EventEnvelope<'message.completed', MessageCompletedData>;
|
||||
export type ToolCallEvent = EventEnvelope<'tool.call', ToolCallData>;
|
||||
export type ToolResultEvent = EventEnvelope<'tool.result', ToolResultData>;
|
||||
export type ExecutionUpdatedEvent = EventEnvelope<'execution.updated', ExecutionUpdatedData>;
|
||||
export type ApprovalRequestedEvent = EventEnvelope<'approval.requested', ApprovalRequestedData>;
|
||||
export type ApprovalResolvedEvent = EventEnvelope<'approval.resolved', ApprovalResolvedData>;
|
||||
export type RunStateEvent = EventEnvelope<'run.state', RunStateData>;
|
||||
export type SessionUpdatedEvent = EventEnvelope<'session.updated', SessionUpdatedData>;
|
||||
export type ErrorEvent = EventEnvelope<'error', ErrorData>;
|
||||
export type HeartbeatEvent = EventEnvelope<'heartbeat', Record<string, never>>;
|
||||
|
||||
/** 平台事件:客户端唯一认识的事件形态 */
|
||||
export type PlatformEvent =
|
||||
| MessageDeltaEvent
|
||||
| MessageCompletedEvent
|
||||
| ToolCallEvent
|
||||
| ToolResultEvent
|
||||
| ExecutionUpdatedEvent
|
||||
| ApprovalRequestedEvent
|
||||
| ApprovalResolvedEvent
|
||||
| RunStateEvent
|
||||
| SessionUpdatedEvent
|
||||
| ErrorEvent
|
||||
| HeartbeatEvent;
|
||||
|
||||
export type PlatformEventType = PlatformEvent['type'];
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
{"v":1,"seq":1,"ts":1757404800000,"sessionId":"s-1","runId":"r-1","type":"message.delta","data":{"messageId":"m-1","text":"正在核算产能"}}
|
||||
{"v":1,"seq":2,"ts":1757404800100,"sessionId":"s-1","runId":"r-1","type":"message.completed","data":{"messageId":"m-1","finishReason":"stop"}}
|
||||
{"v":1,"seq":3,"ts":1757404800200,"sessionId":"s-1","runId":"r-1","type":"tool.call","data":{"callId":"c-1","capabilityId":"aps.schedule.preview","argsDigest":"sha256:9f2a"}}
|
||||
{"v":1,"seq":4,"ts":1757404800300,"sessionId":"s-1","runId":"r-1","type":"tool.result","data":{"callId":"c-1","executionId":"exec-1","status":"accepted"}}
|
||||
{"v":1,"seq":5,"ts":1757404800400,"sessionId":"s-1","runId":"r-1","type":"execution.updated","data":{"executionId":"exec-1","status":"succeeded"}}
|
||||
{"v":1,"seq":6,"ts":1757404800500,"sessionId":"s-1","runId":"r-1","type":"approval.requested","data":{"approvalId":"apr-1","capabilityId":"aps.schedule.publish","paramsDigest":"sha256:1c4d","impactSummary":"发布 2026-10 排产方案,影响 12 个订单","revision":37,"expiresAt":1757408400000}}
|
||||
{"v":1,"seq":7,"ts":1757404800600,"sessionId":"s-1","runId":"r-1","type":"approval.resolved","data":{"approvalId":"apr-1","decision":"approved"}}
|
||||
{"v":1,"seq":8,"ts":1757404800700,"sessionId":"s-1","runId":"r-1","type":"run.state","data":{"state":"idle"}}
|
||||
{"v":1,"seq":9,"ts":1757404800800,"sessionId":"s-1","type":"session.updated","data":{"title":"十月排产核对","status":"active"}}
|
||||
{"v":1,"seq":10,"ts":1757404800900,"sessionId":"s-1","type":"error","data":{"code":"provider.timeout","message":"APS 求解超时","retryable":true,"errorKind":"timeout"}}
|
||||
{"v":1,"seq":11,"ts":1757404801000,"sessionId":"s-1","type":"heartbeat","data":{}}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
{
|
||||
"session": {
|
||||
"schema": "session.schema.json",
|
||||
"value": { "sessionId": "s-1", "projectId": "p-1", "scope": "project", "status": "active", "title": "十月排产核对", "createdAt": 1757404800000 }
|
||||
},
|
||||
"capability": {
|
||||
"schema": "capability.schema.json",
|
||||
"value": {
|
||||
"id": "aps.schedule.preview",
|
||||
"providerId": "aps",
|
||||
"title": "试排一版",
|
||||
"purpose": "按当前项目数据试排一版方案,只出结果不发布",
|
||||
"effect": "read",
|
||||
"synonyms": ["试排", "跑一版", "预排"]
|
||||
}
|
||||
},
|
||||
"receipt": {
|
||||
"schema": "receipt.schema.json",
|
||||
"value": { "executionId": "exec-1", "status": "pending-confirmation", "approvalId": "apr-1" }
|
||||
},
|
||||
"approvalDecision": {
|
||||
"schema": "approval.schema.json",
|
||||
"value": { "decision": "approve", "revision": 37, "idempotencyKey": "idem-2f8a1c9d", "note": "按会议结论发布" }
|
||||
},
|
||||
"error": {
|
||||
"schema": "error.schema.json",
|
||||
"value": { "error": { "code": "auth.expired", "message": "登录态已过期", "retryable": false } }
|
||||
}
|
||||
}
|
||||
|
|
@ -2,22 +2,10 @@
|
|||
* 客户端与服务端共享的契约(架构设计第 2 节)。
|
||||
*
|
||||
* 只放类型与模式定义,不引入运行时依赖:这一层被所有包依赖,任何依赖都会变成全局耦合。
|
||||
* 引擎差异不在这里体现——引擎事件经适配层转成下面的平台事件,界面对引擎无感。
|
||||
* JSON Schema 在 contracts/schemas/ 是规范本体,本包里的 TS 是镜像;两者由
|
||||
* scripts/check-contracts.test.mjs 卡漂移。
|
||||
*/
|
||||
export * from './events.js';
|
||||
export * from './protocol.js';
|
||||
|
||||
/** 能力标识,形如 `aps.schedule.preview`。命名与描述规范见 docs/spec/capability-description.md */
|
||||
export type CapabilityId = string;
|
||||
|
||||
/** 执行回执:所有能力调用的统一返回(架构设计 4.2) */
|
||||
export interface ExecutionReceipt {
|
||||
executionId: string;
|
||||
status: 'accepted' | 'succeeded' | 'failed' | 'pending-confirmation';
|
||||
result?: unknown;
|
||||
errorKind?: string;
|
||||
}
|
||||
|
||||
/** 平台事件:唯一面向客户端的事件形态(实施设计 1.7 定义事件模型) */
|
||||
export type PlatformEvent =
|
||||
| { type: 'message.delta'; sessionId: string; text: string }
|
||||
| { type: 'tool.call'; sessionId: string; capabilityId: CapabilityId }
|
||||
| { type: 'receipt'; sessionId: string; receipt: ExecutionReceipt };
|
||||
export const CONTRACTS_PACKAGE = '@aps/contracts';
|
||||
|
|
@ -0,0 +1,142 @@
|
|||
/**
|
||||
* 协议 v1 的 HTTP 表面(JSON Schema 为准,这里是 TS 镜像)。
|
||||
*
|
||||
* 服务端与客户端之间只认这一套接口;引擎协议(Pi 的 JSONL、Codex 的 app-server)不越过适配层。
|
||||
* 端点与规则见 docs/spec/protocol.md。
|
||||
*/
|
||||
|
||||
/** 路径前缀:协议改版就换前缀,不原地改语义 */
|
||||
export const PROTOCOL_PREFIX = '/v1' as const;
|
||||
|
||||
export const ENDPOINTS = {
|
||||
health: '/v1/health',
|
||||
me: '/v1/me',
|
||||
projects: '/v1/projects',
|
||||
sessions: '/v1/sessions',
|
||||
session: (sessionId: string) => `/v1/sessions/${sessionId}`,
|
||||
/** SSE:回放 + 实时事件流 */
|
||||
sessionEvents: (sessionId: string) => `/v1/sessions/${sessionId}/events`,
|
||||
sessionMessages: (sessionId: string) => `/v1/sessions/${sessionId}/messages`,
|
||||
sessionCancel: (sessionId: string) => `/v1/sessions/${sessionId}/cancel`,
|
||||
capabilities: '/v1/capabilities',
|
||||
execution: (executionId: string) => `/v1/executions/${executionId}`,
|
||||
approval: (approvalId: string) => `/v1/approvals/${approvalId}`,
|
||||
} as const;
|
||||
|
||||
/** 能力标识,形如 `aps.schedule.preview` */
|
||||
export type CapabilityId = string;
|
||||
|
||||
export interface HealthResponse {
|
||||
status: 'ok';
|
||||
version: string;
|
||||
protocolVersion: number;
|
||||
}
|
||||
|
||||
export interface MeResponse {
|
||||
userId: string;
|
||||
tenantId: string;
|
||||
displayName: string;
|
||||
/** 桌面端与浏览器共用同一身份模型,登录方式不同(见协议文档第 2 节) */
|
||||
clientKind: 'desktop' | 'browser';
|
||||
}
|
||||
|
||||
/** 能力条目:按身份过滤后返回;描述七要素见 docs/spec/capability-description.md */
|
||||
export interface CapabilitySummary {
|
||||
id: CapabilityId;
|
||||
providerId: string;
|
||||
title: string;
|
||||
purpose: string;
|
||||
effect: 'read' | 'draft' | 'write';
|
||||
inputSchema?: Record<string, unknown>;
|
||||
synonyms?: string[];
|
||||
}
|
||||
|
||||
export interface CapabilitiesResponse {
|
||||
capabilities: CapabilitySummary[];
|
||||
}
|
||||
|
||||
/** 会话概要(ADR-0003:个人会话只读,写操作必须在项目会话内) */
|
||||
export interface SessionSummary {
|
||||
sessionId: string;
|
||||
projectId: string | null;
|
||||
scope: 'personal' | 'project';
|
||||
status: 'active' | 'archived';
|
||||
title: string;
|
||||
createdAt: number;
|
||||
}
|
||||
|
||||
export interface CreateSessionRequest {
|
||||
projectId?: string;
|
||||
scope: 'personal' | 'project';
|
||||
title?: string;
|
||||
}
|
||||
|
||||
export interface CreateSessionResponse {
|
||||
session: SessionSummary;
|
||||
}
|
||||
|
||||
export interface SessionListResponse {
|
||||
sessions: SessionSummary[];
|
||||
}
|
||||
|
||||
export interface SessionDetailResponse {
|
||||
session: SessionSummary;
|
||||
/** 最近一段消息;完整历史走事件流回放 */
|
||||
messages: ChatMessage[];
|
||||
}
|
||||
|
||||
export interface ChatMessage {
|
||||
messageId: string;
|
||||
role: 'user' | 'assistant' | 'system';
|
||||
ts: number;
|
||||
text: string;
|
||||
}
|
||||
|
||||
export interface SendMessageRequest {
|
||||
text: string;
|
||||
/** 同一写请求重试不重复生效 */
|
||||
idempotencyKey: string;
|
||||
}
|
||||
|
||||
export interface SendMessageResponse {
|
||||
accepted: true;
|
||||
messageId: string;
|
||||
runId: string;
|
||||
}
|
||||
|
||||
export interface CancelResponse {
|
||||
state: 'cancelling' | 'idle';
|
||||
}
|
||||
|
||||
/** 能力调用的统一回执(架构设计 4.2) */
|
||||
export interface ExecutionReceipt {
|
||||
executionId: string;
|
||||
status: 'accepted' | 'pending-confirmation' | 'succeeded' | 'failed';
|
||||
result?: unknown;
|
||||
errorKind?: string;
|
||||
/** status = pending-confirmation 时给出待确认动作标识 */
|
||||
approvalId?: string;
|
||||
}
|
||||
|
||||
export interface ApprovalDecisionRequest {
|
||||
decision: 'approve' | 'reject';
|
||||
/** 目标数据版本;版本变了确认就失效 */
|
||||
revision: number;
|
||||
idempotencyKey: string;
|
||||
note?: string;
|
||||
}
|
||||
|
||||
export interface ApprovalDecisionResponse {
|
||||
approvalId: string;
|
||||
decision: 'approved' | 'rejected' | 'expired';
|
||||
receipt?: ExecutionReceipt;
|
||||
}
|
||||
|
||||
export interface ErrorEnvelope {
|
||||
error: {
|
||||
code: string;
|
||||
message: string;
|
||||
retryable: boolean;
|
||||
detail?: unknown;
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://aps.local/contracts/approval.schema.json",
|
||||
"title": "ApprovalDecisionRequest",
|
||||
"description": "客户端对确认卡的响应(架构设计 4.2:确认卡防的是误解意图,不是防越权)。",
|
||||
"type": "object",
|
||||
"required": ["decision", "revision", "idempotencyKey"],
|
||||
"properties": {
|
||||
"decision": { "type": "string", "enum": ["approve", "reject"] },
|
||||
"revision": { "type": "integer", "minimum": 0 },
|
||||
"idempotencyKey": { "type": "string", "minLength": 8 },
|
||||
"note": { "type": "string" }
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://aps.local/contracts/capability.schema.json",
|
||||
"title": "CapabilitySummary",
|
||||
"description": "按身份过滤后交给客户端与引擎的能力条目。描述七要素见 docs/spec/capability-description.md。",
|
||||
"type": "object",
|
||||
"required": ["id", "providerId", "title", "purpose", "effect"],
|
||||
"properties": {
|
||||
"id": { "type": "string", "pattern": "^[a-z0-9]+(\\.[a-z0-9-]+)+$" },
|
||||
"providerId": { "type": "string" },
|
||||
"title": { "type": "string" },
|
||||
"purpose": { "type": "string", "description": "一句话用途,40 字以内" },
|
||||
"effect": { "type": "string", "enum": ["read", "draft", "write"] },
|
||||
"inputSchema": { "type": "object" },
|
||||
"synonyms": { "type": "array", "items": { "type": "string" }, "description": "业务黑话与同义词" }
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://aps.local/contracts/envelope.schema.json",
|
||||
"title": "EventEnvelope",
|
||||
"description": "服务端推给客户端的事件信封(协议 v1)。界面只依赖这一层,不感知引擎差异。",
|
||||
"$defs": {
|
||||
"status": {
|
||||
"type": "string",
|
||||
"enum": ["accepted", "pending-confirmation", "succeeded", "failed"]
|
||||
},
|
||||
"envelope": {
|
||||
"type": "object",
|
||||
"required": ["v", "seq", "ts", "sessionId", "type", "data"],
|
||||
"properties": {
|
||||
"v": { "const": 1, "description": "协议版本;改协议就加版本,不改这一版" },
|
||||
"seq": { "type": "integer", "minimum": 1, "description": "会话内单调递增;断点续传与去重靠它" },
|
||||
"ts": { "type": "integer", "description": "发生时间,整数毫秒(ADR-0007)" },
|
||||
"sessionId": { "type": "string", "minLength": 1 },
|
||||
"runId": { "type": "string", "description": "一次运行(回合)的标识;同会话可先后多个 run" },
|
||||
"type": { "type": "string" },
|
||||
"data": { "type": "object" }
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://aps.local/contracts/error.schema.json",
|
||||
"title": "ErrorEnvelope",
|
||||
"description": "HTTP 错误统一形状;SSE 里的错误见 events.schema.json 的 error 事件。",
|
||||
"type": "object",
|
||||
"required": ["error"],
|
||||
"properties": {
|
||||
"error": {
|
||||
"type": "object",
|
||||
"required": ["code", "message", "retryable"],
|
||||
"properties": {
|
||||
"code": { "type": "string" },
|
||||
"message": { "type": "string" },
|
||||
"retryable": { "type": "boolean" },
|
||||
"detail": {}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
|
|
@ -0,0 +1,191 @@
|
|||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://aps.local/contracts/events.schema.json",
|
||||
"title": "PlatformEvent",
|
||||
"description": "协议 v1 的全部事件类型。新增事件类型必须同时改这里、contracts/events.ts 与 docs/spec/protocol.md。",
|
||||
"$ref": "#/$defs/event",
|
||||
"$defs": {
|
||||
"envelope": {
|
||||
"allOf": [{ "$ref": "envelope.schema.json#/$defs/envelope" }]
|
||||
},
|
||||
"messageDelta": {
|
||||
"allOf": [{ "$ref": "#/$defs/envelope" }],
|
||||
"properties": {
|
||||
"type": { "const": "message.delta" },
|
||||
"data": {
|
||||
"type": "object",
|
||||
"required": ["messageId", "text"],
|
||||
"properties": {
|
||||
"messageId": { "type": "string" },
|
||||
"text": { "type": "string" }
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"messageCompleted": {
|
||||
"allOf": [{ "$ref": "#/$defs/envelope" }],
|
||||
"properties": {
|
||||
"type": { "const": "message.completed" },
|
||||
"data": {
|
||||
"type": "object",
|
||||
"required": ["messageId"],
|
||||
"properties": {
|
||||
"messageId": { "type": "string" },
|
||||
"finishReason": { "type": "string", "enum": ["stop", "cancelled", "error", "length"] }
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"toolCall": {
|
||||
"allOf": [{ "$ref": "#/$defs/envelope" }],
|
||||
"properties": {
|
||||
"type": { "const": "tool.call" },
|
||||
"data": {
|
||||
"type": "object",
|
||||
"required": ["callId", "capabilityId", "argsDigest"],
|
||||
"properties": {
|
||||
"callId": { "type": "string" },
|
||||
"capabilityId": { "type": "string" },
|
||||
"argsDigest": { "type": "string", "description": "参数摘要,不是参数明文" }
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"toolResult": {
|
||||
"allOf": [{ "$ref": "#/$defs/envelope" }],
|
||||
"properties": {
|
||||
"type": { "const": "tool.result" },
|
||||
"data": {
|
||||
"type": "object",
|
||||
"required": ["callId", "executionId", "status"],
|
||||
"properties": {
|
||||
"callId": { "type": "string" },
|
||||
"executionId": { "type": "string" },
|
||||
"status": { "$ref": "envelope.schema.json#/$defs/status" }
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"executionUpdated": {
|
||||
"allOf": [{ "$ref": "#/$defs/envelope" }],
|
||||
"properties": {
|
||||
"type": { "const": "execution.updated" },
|
||||
"data": {
|
||||
"type": "object",
|
||||
"required": ["executionId", "status"],
|
||||
"properties": {
|
||||
"executionId": { "type": "string" },
|
||||
"status": { "$ref": "envelope.schema.json#/$defs/status" },
|
||||
"errorKind": { "type": "string" }
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"approvalRequested": {
|
||||
"allOf": [{ "$ref": "#/$defs/envelope" }],
|
||||
"properties": {
|
||||
"type": { "const": "approval.requested" },
|
||||
"data": {
|
||||
"type": "object",
|
||||
"required": ["approvalId", "capabilityId", "paramsDigest", "impactSummary", "revision", "expiresAt"],
|
||||
"properties": {
|
||||
"approvalId": { "type": "string" },
|
||||
"capabilityId": { "type": "string" },
|
||||
"paramsDigest": { "type": "string" },
|
||||
"impactSummary": { "type": "string", "description": "确认卡正文:影响范围与关键参数摘要" },
|
||||
"revision": { "type": "integer", "minimum": 0, "description": "目标数据版本;版本变了确认就失效" },
|
||||
"expiresAt": { "type": "integer" }
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"approvalResolved": {
|
||||
"allOf": [{ "$ref": "#/$defs/envelope" }],
|
||||
"properties": {
|
||||
"type": { "const": "approval.resolved" },
|
||||
"data": {
|
||||
"type": "object",
|
||||
"required": ["approvalId", "decision"],
|
||||
"properties": {
|
||||
"approvalId": { "type": "string" },
|
||||
"decision": { "type": "string", "enum": ["approved", "rejected", "expired"] }
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"runState": {
|
||||
"allOf": [{ "$ref": "#/$defs/envelope" }],
|
||||
"properties": {
|
||||
"type": { "const": "run.state" },
|
||||
"data": {
|
||||
"type": "object",
|
||||
"required": ["state"],
|
||||
"properties": {
|
||||
"state": { "type": "string", "enum": ["idle", "running", "cancelling", "cancelled"] }
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"sessionUpdated": {
|
||||
"allOf": [{ "$ref": "#/$defs/envelope" }],
|
||||
"properties": {
|
||||
"type": { "const": "session.updated" },
|
||||
"data": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"title": { "type": "string" },
|
||||
"status": { "type": "string", "enum": ["active", "archived"] }
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"allOf": [{ "$ref": "#/$defs/envelope" }],
|
||||
"properties": {
|
||||
"type": { "const": "error" },
|
||||
"data": {
|
||||
"type": "object",
|
||||
"required": ["code", "message", "retryable"],
|
||||
"properties": {
|
||||
"code": { "type": "string" },
|
||||
"message": { "type": "string" },
|
||||
"retryable": { "type": "boolean" },
|
||||
"errorKind": { "type": "string" }
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"heartbeat": {
|
||||
"allOf": [{ "$ref": "#/$defs/envelope" }],
|
||||
"properties": {
|
||||
"type": { "const": "heartbeat" },
|
||||
"data": { "type": "object", "additionalProperties": false }
|
||||
}
|
||||
},
|
||||
"event": {
|
||||
"oneOf": [
|
||||
{ "$ref": "#/$defs/messageDelta" },
|
||||
{ "$ref": "#/$defs/messageCompleted" },
|
||||
{ "$ref": "#/$defs/toolCall" },
|
||||
{ "$ref": "#/$defs/toolResult" },
|
||||
{ "$ref": "#/$defs/executionUpdated" },
|
||||
{ "$ref": "#/$defs/approvalRequested" },
|
||||
{ "$ref": "#/$defs/approvalResolved" },
|
||||
{ "$ref": "#/$defs/runState" },
|
||||
{ "$ref": "#/$defs/sessionUpdated" },
|
||||
{ "$ref": "#/$defs/error" },
|
||||
{ "$ref": "#/$defs/heartbeat" }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://aps.local/contracts/receipt.schema.json",
|
||||
"title": "ExecutionReceipt",
|
||||
"description": "能力调用的统一回执(架构设计 4.2)。",
|
||||
"type": "object",
|
||||
"required": ["executionId", "status"],
|
||||
"properties": {
|
||||
"executionId": { "type": "string" },
|
||||
"status": { "$ref": "envelope.schema.json#/$defs/status" },
|
||||
"result": {},
|
||||
"errorKind": { "type": "string" },
|
||||
"approvalId": { "type": "string", "description": "status = pending-confirmation 时给出待确认动作标识" }
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://aps.local/contracts/session.schema.json",
|
||||
"title": "SessionSummary",
|
||||
"description": "会话概要(ADR-0003:个人会话只读,写操作必须在项目会话内)。",
|
||||
"type": "object",
|
||||
"required": ["sessionId", "scope", "status", "title", "createdAt"],
|
||||
"properties": {
|
||||
"sessionId": { "type": "string" },
|
||||
"projectId": { "type": ["string", "null"] },
|
||||
"scope": { "type": "string", "enum": ["personal", "project"] },
|
||||
"status": { "type": "string", "enum": ["active", "archived"] },
|
||||
"title": { "type": "string" },
|
||||
"createdAt": { "type": "integer" }
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
|
|
@ -5,5 +5,5 @@
|
|||
"outDir": "dist",
|
||||
"tsBuildInfoFile": "dist/.tsbuildinfo"
|
||||
},
|
||||
"include": ["index.ts"]
|
||||
"include": ["*.ts"]
|
||||
}
|
||||
|
|
@ -0,0 +1,107 @@
|
|||
# 客户端与服务端协议(v1)
|
||||
|
||||
> 契约本体在 `contracts/schemas/`(JSON Schema),TS 镜像在 `contracts/`,漂移由 `scripts/check-contracts.test.mjs` 卡。本文是给人看的同一份东西。
|
||||
|
||||
## 1 范围与所有权
|
||||
|
||||
服务端与客户端之间只认这一套接口。引擎协议(Pi 的 JSONL、Codex 的 app-server)只活在 `server/agent` 内部,客户端与 Core 都不感知——协议的所有权比协议的兼容性重要(架构设计 5.2)。
|
||||
|
||||
界面只依赖本文的事件模型:换引擎、换模型,事件不变。
|
||||
|
||||
## 2 版本与兼容规则
|
||||
|
||||
| 规则 | 说明 |
|
||||
| --- | --- |
|
||||
| 路径版本 | 前缀 `{PROTOCOL_PREFIX}` = `/v1`;改语义就换前缀,不原地改 |
|
||||
| 事件版本 | 每个事件带 `v`,当前 `1`;字段只加不改不删 |
|
||||
| 删除字段 | 先废弃(保留但不再填充)→ 下一个大版本再删 |
|
||||
| 未知事件 | 客户端必须忽略不认识的 `type`,不能崩 |
|
||||
| 未知字段 | 客户端必须忽略,不能因多字段报错 |
|
||||
|
||||
## 3 认证与身份
|
||||
|
||||
| 客户端 | 方式 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| 浏览器 | `Cookie: ap_session=<httpOnly>` | 沿用现有 JMS 登录;服务端不把业务系统 token 下发给客户端 |
|
||||
| 桌面端 | `Authorization: Bearer <desktop-token>` | 授权码 + 设备绑定换来的短期令牌(沿用现有授权方式) |
|
||||
|
||||
两者都由 `server/entry` 解析成同一个 Subject(用户 + 租户),再交给应用服务与 Core。首次调用的返回值就是 `MeResponse`。
|
||||
|
||||
## 4 端点
|
||||
|
||||
| 方法 | 路径 | 请求 | 响应 | 说明 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| GET | `/v1/health` | — | `HealthResponse` | 健康检查 |
|
||||
| GET | `/v1/me` | — | `MeResponse` | 当前身份与客户端形态 |
|
||||
| GET | `/v1/projects` | — | `ProjectSummary[]` | 可见项目 |
|
||||
| GET | `/v1/sessions` | `?projectId=` | `SessionListResponse` | 会话列表 |
|
||||
| POST | `/v1/sessions` | `CreateSessionRequest` | `CreateSessionResponse` | 建会话;写操作必须建在项目会话(ADR-0003) |
|
||||
| GET | `/v1/sessions/{id}` | `?limit=` | `SessionDetailResponse` | 会话详情与最近消息 |
|
||||
| GET | `/v1/sessions/{id}/events` | `Last-Event-ID` 或 `?since=` | SSE 事件流 | 回放 + 实时,见第 5 节 |
|
||||
| POST | `/v1/sessions/{id}/messages` | `SendMessageRequest` | `SendMessageResponse` | 发消息;返回受理,结果走事件流 |
|
||||
| POST | `/v1/sessions/{id}/cancel` | — | `CancelResponse` | 取消当前运行 |
|
||||
| GET | `/v1/capabilities` | `?sessionId=` | `CapabilitiesResponse` | 按身份过滤的能力清单 |
|
||||
| GET | `/v1/executions/{executionId}` | — | `ExecutionReceipt` | 执行状态与回执 |
|
||||
| POST | `/v1/approvals/{approvalId}` | `ApprovalDecisionRequest` | `ApprovalDecisionResponse` | 确认卡响应 |
|
||||
|
||||
## 5 事件流(SSE)
|
||||
|
||||
| 规则 | 说明 |
|
||||
| --- | --- |
|
||||
| 传输 | `text/event-stream`;`event:` 填事件 `type`,`data:` 填整条事件 JSON |
|
||||
| 序号 | `seq` 在会话内单调递增,从 1 开始;客户端按它去重与排序 |
|
||||
| 断点续传 | 客户端带 `Last-Event-ID`(或 `?since=`)重连,服务端从该序号之后回放 |
|
||||
| 回放 | 回放与实时用同一格式,客户端不需要两套解析 |
|
||||
| 心跳 | 无事件时按固定间隔发 `heartbeat`,用于探活与代理保活 |
|
||||
| 顺序 | 同一会话的事件严格有序;不同会话之间不保证 |
|
||||
| 敏感 | 事件里只有参数摘要(`argsDigest`、`paramsDigest`),没有凭据与参数明文 |
|
||||
|
||||
## 6 事件清单
|
||||
|
||||
| 事件 | 含义 | `data` 关键字段 |
|
||||
| --- | --- | --- |
|
||||
| `message.delta` | 助手增量文本 | `messageId`、`text` |
|
||||
| `message.completed` | 一条助手消息结束 | `messageId`、`finishReason` |
|
||||
| `tool.call` | 引擎请求调用能力 | `callId`、`capabilityId`、`argsDigest` |
|
||||
| `tool.result` | 能力调用的回执 | `callId`、`executionId`、`status` |
|
||||
| `execution.updated` | 执行状态变化 | `executionId`、`status`、`errorKind` |
|
||||
| `approval.requested` | 需要用户确认 | `approvalId`、`capabilityId`、`paramsDigest`、`impactSummary`、`revision`、`expiresAt` |
|
||||
| `approval.resolved` | 确认结果 | `approvalId`、`decision` |
|
||||
| `run.state` | 当前运行状态 | `state`(`idle`/`running`/`cancelling`/`cancelled`) |
|
||||
| `session.updated` | 会话标题或状态变化 | `title`、`status` |
|
||||
| `error` | 错误 | `code`、`message`、`retryable` |
|
||||
| `heartbeat` | 保活 | 空对象 |
|
||||
|
||||
`status` 取值统一为 `accepted` / `pending-confirmation` / `succeeded` / `failed`。
|
||||
|
||||
## 7 审批时序
|
||||
|
||||
1. 引擎请求写能力 → Core 判定需确认 → 落一条待确认动作(ADR-0003、架构设计 4.2)
|
||||
2. 服务端推 `approval.requested`,客户端渲染确认卡(影响范围、参数摘要、目标版本、有效期)
|
||||
3. 用户点批准 → `POST /v1/approvals/{id}`,带 `revision` 与 `idempotencyKey`
|
||||
4. 服务端校验版本与有效期 → 推 `approval.resolved` → 执行 → 推 `execution.updated`
|
||||
5. 版本过期或已执行 → 返回 `expired`,客户端提示重新确认;确认人是会话发起人本人
|
||||
|
||||
## 8 错误模型
|
||||
|
||||
| 位置 | 形状 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| HTTP | `ErrorEnvelope` | 请求本身失败(鉴权、参数、找不到) |
|
||||
| SSE | `error` 事件 | 运行中失败;`retryable` 表示可重试,客户端据此给"重试"按钮 |
|
||||
|
||||
## 9 幂等与取消
|
||||
|
||||
| 场景 | 规则 |
|
||||
| --- | --- |
|
||||
| 发消息 | 带 `idempotencyKey`;同一 key 重试不产生第二个 run |
|
||||
| 审批 | 带 `idempotencyKey`;同一 key 只生效一次 |
|
||||
| 取消 | 幂等:重复取消不报错;已结束的运行返回 `idle` |
|
||||
| 结果未知 | 执行超时返回 `failed` 且 `errorKind = unknown`,由客户端提示查执行记录,不自动重试写操作 |
|
||||
|
||||
## 10 变更规则
|
||||
|
||||
改协议必须同时改三处,否则 `pnpm test` 会失败:
|
||||
|
||||
1. `contracts/schemas/*.json`(规范本体)
|
||||
2. `contracts/events.ts` / `contracts/protocol.ts`(TS 镜像)
|
||||
3. 本文档(第 4、6 节的表)
|
||||
|
|
@ -13,6 +13,7 @@
|
|||
"test": "node --test \"scripts/**/*.test.mjs\""
|
||||
},
|
||||
"devDependencies": {
|
||||
"ajv": "^8.20.0",
|
||||
"typescript": "^7.0.2"
|
||||
}
|
||||
}
|
||||
|
|
@ -8,6 +8,9 @@ importers:
|
|||
|
||||
.:
|
||||
devDependencies:
|
||||
ajv:
|
||||
specifier: ^8.20.0
|
||||
version: 8.20.0
|
||||
typescript:
|
||||
specifier: ^7.0.2
|
||||
version: 7.0.2
|
||||
|
|
@ -168,6 +171,22 @@ packages:
|
|||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
ajv@8.20.0:
|
||||
resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==}
|
||||
|
||||
fast-deep-equal@3.1.3:
|
||||
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
|
||||
|
||||
fast-uri@3.1.8:
|
||||
resolution: {integrity: sha512-GZMtZUTNRpOVIECoXwLNZS5xUGE+mVNbTB8h/7Rwh2TFWcBQiPzTgyZi05BF9UMZKkLJv8XBRJTlU7zg8+ZfMg==}
|
||||
|
||||
json-schema-traverse@1.0.0:
|
||||
resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==}
|
||||
|
||||
require-from-string@2.0.2:
|
||||
resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
typescript@7.0.2:
|
||||
resolution: {integrity: sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA==}
|
||||
engines: {node: '>=16.20.0'}
|
||||
|
|
@ -235,6 +254,21 @@ snapshots:
|
|||
'@typescript/typescript-win32-x64@7.0.2':
|
||||
optional: true
|
||||
|
||||
ajv@8.20.0:
|
||||
dependencies:
|
||||
fast-deep-equal: 3.1.3
|
||||
fast-uri: 3.1.8
|
||||
json-schema-traverse: 1.0.0
|
||||
require-from-string: 2.0.2
|
||||
|
||||
fast-deep-equal@3.1.3: {}
|
||||
|
||||
fast-uri@3.1.8: {}
|
||||
|
||||
json-schema-traverse@1.0.0: {}
|
||||
|
||||
require-from-string@2.0.2: {}
|
||||
|
||||
typescript@7.0.2:
|
||||
optionalDependencies:
|
||||
'@typescript/typescript-aix-ppc64': 7.0.2
|
||||
|
|
|
|||
|
|
@ -0,0 +1,87 @@
|
|||
/**
|
||||
* 契约漂移检查(任务 A1.7)。
|
||||
*
|
||||
* 三件事:
|
||||
* 1. 事件与 HTTP 样例必须符合 contracts/schemas 下的 JSON Schema
|
||||
* 2. 事件类型在 schema、TS 镜像、协议文档三处必须完全一致
|
||||
* 3. 每种事件类型都要有样例,避免新增类型忘了配样例
|
||||
*/
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFile, readdir } from 'node:fs/promises';
|
||||
import { join } from 'node:path';
|
||||
import { test } from 'node:test';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
import Ajv2020 from 'ajv/dist/2020.js';
|
||||
|
||||
const ROOT = fileURLToPath(new URL('..', import.meta.url));
|
||||
const SCHEMA_DIR = join(ROOT, 'contracts', 'schemas');
|
||||
const EVENTS_SCHEMA_ID = 'https://aps.local/contracts/events.schema.json';
|
||||
|
||||
const readText = (rel) => readFile(join(ROOT, rel), 'utf8');
|
||||
|
||||
async function buildAjv() {
|
||||
const ajv = new Ajv2020({ allErrors: true, strict: false });
|
||||
for (const name of await readdir(SCHEMA_DIR)) {
|
||||
if (!name.endsWith('.json')) continue;
|
||||
ajv.addSchema(JSON.parse(await readFile(join(SCHEMA_DIR, name), 'utf8')));
|
||||
}
|
||||
return ajv;
|
||||
}
|
||||
|
||||
const formatErrors = (errors) => (errors ?? []).map((e) => `${e.instancePath || '/'} ${e.message}`).join('; ');
|
||||
|
||||
test('事件样例符合 events.schema.json', async () => {
|
||||
const ajv = await buildAjv();
|
||||
const validate = ajv.getSchema(EVENTS_SCHEMA_ID);
|
||||
assert.ok(validate, '未注册 events.schema.json');
|
||||
const lines = (await readText('contracts/fixtures/events.jsonl')).split(/\r?\n/).filter((l) => l.trim());
|
||||
assert.ok(lines.length >= 11, `样例太少:${lines.length}`);
|
||||
for (const line of lines) {
|
||||
const event = JSON.parse(line);
|
||||
assert.ok(validate(event), `${event.type} 样例不符合 schema:${formatErrors(validate.errors)}`);
|
||||
}
|
||||
});
|
||||
|
||||
test('HTTP 样例符合各自 schema', async () => {
|
||||
const ajv = await buildAjv();
|
||||
const cases = JSON.parse(await readText('contracts/fixtures/http.json'));
|
||||
for (const [name, testCase] of Object.entries(cases)) {
|
||||
const schemaId = `https://aps.local/contracts/${testCase.schema}`;
|
||||
const validate = ajv.getSchema(schemaId);
|
||||
assert.ok(validate, `${name}:未注册 ${testCase.schema}`);
|
||||
assert.ok(validate(testCase.value), `${name} 样例不符合 ${testCase.schema}:${formatErrors(validate.errors)}`);
|
||||
}
|
||||
});
|
||||
|
||||
test('事件类型在 schema、TS 与协议文档三处一致', async () => {
|
||||
const schemaTypes = new Set(
|
||||
[...(await readText('contracts/schemas/events.schema.json')).matchAll(/"const":\s*"([a-z][a-z0-9]*(?:\.[a-z][a-z0-9-]*)*)"/g)].map((m) => m[1]),
|
||||
);
|
||||
const tsTypes = new Set([...(await readText('contracts/events.ts')).matchAll(/EventEnvelope<'([^']+)'/g)].map((m) => m[1]));
|
||||
const docTypes = new Set([...(await readText('docs/spec/protocol.md')).matchAll(/^\| `([a-z][a-z0-9]*(?:\.[a-z][a-z0-9-]*)*)` \|/gm)].map((m) => m[1]));
|
||||
|
||||
assert.ok(schemaTypes.size >= 11, `schema 里事件类型太少:${schemaTypes.size}`);
|
||||
const diff = (a, b, label) => [...a].filter((t) => !b.has(t)).map((t) => `${label}:${t}`);
|
||||
const problems = [
|
||||
...diff(schemaTypes, tsTypes, '只在 schema'),
|
||||
...diff(tsTypes, schemaTypes, '只在 TS'),
|
||||
...diff(schemaTypes, docTypes, '只在 schema(文档缺)'),
|
||||
...diff(docTypes, schemaTypes, '只在文档'),
|
||||
];
|
||||
assert.deepEqual(problems, [], `事件类型不一致:\n${problems.join('\n')}`);
|
||||
});
|
||||
|
||||
test('每种事件类型都有样例', async () => {
|
||||
const fixtureTypes = new Set(
|
||||
(await readText('contracts/fixtures/events.jsonl'))
|
||||
.split(/\r?\n/)
|
||||
.filter((l) => l.trim())
|
||||
.map((l) => JSON.parse(l).type),
|
||||
);
|
||||
const schemaTypes = new Set(
|
||||
[...(await readText('contracts/schemas/events.schema.json')).matchAll(/"const":\s*"([a-z][a-z0-9]*(?:\.[a-z][a-z0-9-]*)*)"/g)].map((m) => m[1]),
|
||||
);
|
||||
const missing = [...schemaTypes].filter((t) => !fixtureTypes.has(t));
|
||||
assert.deepEqual(missing, [], `以下事件类型缺样例:${missing.join('、')}`);
|
||||
});
|
||||
Loading…
Reference in New Issue