From 7ad0a139bf74cc6c4f3f97b39c31cfa5079f88a4 Mon Sep 17 00:00:00 2001 From: Rogee Date: Wed, 23 Sep 2026 09:57:28 +0800 Subject: [PATCH] docs: organize third-party contracts and MQ messages --- .../{contracts => thirds}/dispatcher-agent.md | 0 docs/thirds/dispatcher-mq.md | 171 +++++++++++++++++ .../mq-only-v1-freeze-proposal.md | 0 docs/{contracts => thirds}/saas-dispatcher.md | 0 docs/thirds/saas-mq.md | 176 ++++++++++++++++++ ...-rabbitmq-oss-dispatcher-agent-timeline.md | 0 6 files changed, 347 insertions(+) rename docs/{contracts => thirds}/dispatcher-agent.md (100%) create mode 100644 docs/thirds/dispatcher-mq.md rename docs/{contracts => thirds}/mq-only-v1-freeze-proposal.md (100%) rename docs/{contracts => thirds}/saas-dispatcher.md (100%) create mode 100644 docs/thirds/saas-mq.md rename docs/{contracts => thirds}/saas-rabbitmq-oss-dispatcher-agent-timeline.md (100%) diff --git a/docs/contracts/dispatcher-agent.md b/docs/thirds/dispatcher-agent.md similarity index 100% rename from docs/contracts/dispatcher-agent.md rename to docs/thirds/dispatcher-agent.md diff --git a/docs/thirds/dispatcher-mq.md b/docs/thirds/dispatcher-mq.md new file mode 100644 index 0000000..9fadbd3 --- /dev/null +++ b/docs/thirds/dispatcher-mq.md @@ -0,0 +1,171 @@ +# Dispatcher → MQ → SaaS 消息定义与 Topic 规则 + +本文定义 Dispatcher 发往 RabbitMQ、供 SaaS 消费的数据结构,以及当前 Dispatcher broker 使用的交换机、队列和 routing key。消息为 UTF-8 JSON,`schema_version` 固定 `2.0`,单条消息体上限 256 KiB。未知字段和不符合对应 payload 结构的消息拒绝。 + +SaaS 发往 Dispatcher 的命令、查询及 AI 配置响应见 [`saas-mq.md`](saas-mq.md)。 + +## Dispatcher 向 SaaS 发送的数据 + +当前有三类输出:业务事件、Service request、Service response。 + +| 类别 | `event_type` / `message_type` | 当前处理情况 | +| --- | --- | --- | +| 业务事件 | `command.result` | 任务接收、任务控制和重放结果 | +| 业务事件 | `call.status`、`call.finished` | 通话状态及结束事实 | +| 业务事件 | `transcript.updated`、`transcript.failed` | 实时文字更新及失败事实 | +| 业务事件 | `contact.opt_out` | 用户拒绝联系事实 | +| 业务事件 | `recording.uploaded` | OSS 上传事实的可靠 MQ 通知 | +| 业务事件 | `recording.failed` | 结构可校验、通话查询快照可保留;当前业务路径未生成该事件 | +| Service request | `ai.config.request` | 结构及持久化入口存在,但当前仅测试调用,正常运行链路未接通 | +| Service response | `command.query.result`、`call.query.result` | 分别响应 SaaS 的 `command.query`、`call.query` | + +### 业务事件信封 + +所有事件字段必填: + +```json +{ + "schema_version": "2.0", + "event_id": "", + "event_type": "recording.uploaded", + "dispatcher_id": "", + "tenant_id": "", + "tenant_key": "", + "trace_id": "", + "occurred_at": "", + "aggregate_type": "call", + "aggregate_id": "", + "aggregate_version": 1, + "payload": {} +} +``` + +| 字段 | 类型 / 规则 | +| --- | --- | +| `schema_version` | 固定 `2.0` | +| `event_id`、`tenant_id`、`trace_id`、`aggregate_id` | 非空 ID,最多 128 个字符;事件 ID 同时作为 AMQP `message_id` | +| `event_type` | 下列八种之一 | +| `dispatcher_id` | 规范小写 UUID v4,由 Dispatcher 填入 | +| `tenant_key` | 原值保留;非空有效 UTF-8,最多 196 字节;不得含独立的 `*` 或 `#` topic 段 | +| `occurred_at` | RFC3339 时间,由 Dispatcher 生成 | +| `aggregate_type` | 非空字符串,最多 64 字符 | +| `aggregate_version` | 正整数,由 Dispatcher 持久状态递增;不能由 Agent 指定 | +| `payload` | 按 `event_type` 使用下述结构;不接受额外字段 | + +### Event payload + +未列为必填的字段为可选;每种 payload 都拒绝未定义字段。ID 为 JSON string,最多 128 个字符;时间字段为 RFC3339 JSON string;计数、版本、时长及偏移量为 JSON integer,标志位为 JSON boolean;枚举字段为 JSON string。 + +| `event_type` | 必填字段 | 可选字段 / 规则 | +| --- | --- | --- | +| `command.result` | `command_id`、`command_type`、`status`、`reason_code`(非空,最多 128 字符) | `task_id`、`task_item_id`、`execution_id`、`call_id`、`requested_task_revision`、`applied_task_revision`、`admission_state`、`resource_reservation_id`、`permit_id`。`command_type` 为 `call.execute`、`task.control`、`call.replay`、`command.replay`;`status` 为 `accepted`、`waiting`、`applied`、`rejected`、`failed`、`unknown`;revision 非负;`admission_state` 为 `open`、`closed`、`draining`、`quarantined`、`unknown` | +| `call.status` | `call_id`、`execution_id`、`call_state`、`call_version`、`attempt_id`、`attempt_state` | `task_id`、`task_item_id`、`route_policy_id`、`caller_profile_id`、`trunk_id`、`cell_id`、`egress_pool_id`、`observed_at`、`reason_code`。`call_state` 为 `queued`、`dialing`、`ringing`、`answered`、`ended`;`attempt_state` 为 `pending`、`active`、`ended`、`unknown`;`call_version >= 1`;`reason_code` 最多 128 字符 | +| `call.finished` | `call_id`、`execution_id`、`call_version`、`outcome`、`started_at`、`ended_at`、`duration_ms`、`reason_code` | `task_id`、`task_item_id`、`attempt_summary`(最多 32 项)、`asset_state`。`outcome` 为 `answered`、`no_answer`、`busy`、`failed`、`opt_out`、`cancelled`、`unknown`;`duration_ms >= 0`;`asset_state` 为 `pending`、`complete`、`failed`、`unknown`。每个 `attempt_summary` 必填 `attempt_id`、`state`(`pending`、`active`、`ended`、`unknown`),可带 `trunk_id`、`cell_id`、`reason_code` | +| `transcript.updated` | `call_id`、`turn_id`、`segment_id`、`role`、`revision`、`text`、`is_final`、`start_ms`、`end_ms`、`playback_state` | `execution_id`。`role` 为 `customer`、`agent`、`system`;`revision >= 1`;`text` 最多 32768 字符;时间偏移非负;`playback_state` 为 `not_applicable`、`generated`、`sent`、`playback_confirmed`、`cancelled`、`unknown` | +| `transcript.failed` | `call_id`、`reason_code`(非空,最多 128 字符)、`retryable` | `segment_id` 或 `affected_segments`(最多 256 个 ID) | +| `recording.uploaded` | `call_id`、`recording_id`、`format`、`channels`、`sample_rate_hz`、`duration_ms`、`size_bytes`、`checksum_sha256`、`upload_id`、`bucket`、`object_key` | 无。`format` 为 `wav`、`raw_pcm`、`pcma`;`channels=1`;采样率 8000–48000 Hz;`duration_ms >= 0`、`size_bytes >= 1`;SHA-256 为小写 64 位十六进制;`bucket` 长度 1–63,`object_key` 长度 1–1024 | +| `recording.failed` | `call_id`、`recording_id`、`stage`、`reason_code`、`retryable` | `next_retry_at`。`stage` 为 `seal`、`request`、`upload`、`complete`、`verify`、`cleanup`;`reason_code` 非空且最多 128 字符;`next_retry_at` 为 RFC3339 时间 | +| `contact.opt_out` | `call_id`、`task_id`、`task_item_id`、`requested_at` | `turn_id`、`segment_id` | + +`recording.uploaded` 只报告 Agent 已直传 OSS 的既成事实,包含 bucket/object key、格式、大小及校验和;不含 TOKEN、密钥或签名 URL。入队成功不代表 SaaS 已处理,也不等待 `verified`、OSS ID 或 SaaS 回执。 + +### Service request:`ai.config.request` + +当前代码定义并能持久化的请求信封如下;正常运行链路尚无生产调用方: + +```json +{ + "schema_version": "2.0", + "message_type": "ai.config.request", + "message_id": "", + "dispatcher_id": "", + "tenant_id": "", + "tenant_key": "", + "trace_id": "", + "issued_at": "", + "not_after": "", + "payload": { "agent_version_id": "" } +} +``` + +所有字段必填;payload 仅含 `agent_version_id`。对应 SaaS 响应 `ai.config.result` 的结构见 [`saas-mq.md`](saas-mq.md)。 + +### Service response:查询结果 + +查询响应共用信封;`not_after` 不出现在响应中: + +```json +{ + "schema_version": "2.0", + "message_type": "command.query.result", + "message_id": "", + "dispatcher_id": "", + "tenant_id": "", + "tenant_key": "", + "trace_id": "", + "issued_at": "", + "correlation_id": "", + "status": "ok", + "reason_code": "ok", + "payload": {} +} +``` + +`message_type` 为 `command.query.result` 或 `call.query.result`;`correlation_id` 等于原请求的 `message_id`。`status` 可为 `ok`、`pending`、`rejected`: + +- `ok`:`reason_code=ok`,payload 为查询结果。 +- `pending`:`reason_code=waiting`,payload 是空 object;当前查询处理路径不生成该状态。 +- `rejected`:原因是 `invalid_request`、`not_found`、`conflict`、`expired`、`not_authorized`、`unavailable`、`unsupported` 之一;payload 为必填 `{ "detail": "...", "retryable": true|false }`。 + +#### `command.query.result` payload + +必填:`command_id`、`command_type`、`tenant_id`、`tenant_key`、`status`、`aggregate_version`、`updated_at`。 + +可选:`task_id`、`execution_id`、`call_id`、`reason_code`、`wait_reason_code`、`accepted_at`、`waiting_since`、`admission_deadline`、`requested_task_revision`、`applied_task_revision`、`task_state`。`aggregate_version >= 1`。当前查询实现返回命令类型、租户绑定、状态、版本及更新时间;有对应事实时附带 `reason_code`、`accepted_at`。 + +#### `call.query.result` payload + +必填: + +- `call_id`、`execution_id`、`call_state`、`call_version`。 +- `attempts`:`call.status` 事件信封数组。 +- `transcript.events`:`transcript.updated` / `transcript.failed` 事件信封数组。 +- `recordings`:`recording.uploaded` / `recording.failed` 事件信封数组。 +- `delivery`:`pending`、`retry`、`dispatching`、`published` 四种 outbox 状态的非负计数。 +- `snapshot_at`:快照生成时间。 + +可选:`task_id`、`task_item_id`、`reason_code`、`outcome`、`started_at`、`ended_at`、`duration_ms`。查询快照按原事件信封返回明细;超过单条 MQ 大小限制时返回 `unavailable`,不返回部分快照。 + +## MQ Topic 与队列规则 + +### 交换机 + +| 名称 | 类型 | 用途 | +| --- | --- | --- | +| `agent-call.dispatchers.v2` | durable topic | SaaS → Dispatcher 命令、查询和 AI 配置响应 | +| `agent-call.saas.v2` | durable topic | Dispatcher → SaaS 事件、查询响应及 AI 配置请求 | +| `agent-call.dead-letter.v2` | durable topic | Dispatcher 入站队列的死信交换机 | + +三个交换机均不自动删除。 + +### 队列与精确 routing key + +| 名称 / 模板 | 用途 | 绑定 | +| --- | --- | --- | +| `agent-call.d..owner.v2` | Dispatcher 身份占用标记;非 durable、exclusive,连接断开后释放 | 无 | +| `agent-call.d..t..v2` | 该 Dispatcher、该租户的 SaaS→D durable inbox | `agent-call.dispatchers.v2`:`d..t..in` | +| `agent-call.d..t..dlq.v2` | 对应 inbox 的 durable dead-letter queue | `agent-call.dead-letter.v2`:同一条 `.in` routing key | +| `agent-call.saas.events.v2` | D→SaaS durable outbound queue | `agent-call.saas.v2`:每个 D/租户各自的 `d..t..out` | + +- `` 由部署提供,同一 Dispatcher 重启时复用,格式必须是规范小写 UUID v4;部署需保证全局唯一。独占 owner 队列可阻止同一 RabbitMQ broker 上并发重复占用该 ID,但不能证明跨 broker 全局唯一。SaaS 必须按目标 Dispatcher 的 ID 投递,不能广播后再靠消息正文筛选。 +- `` 保留原值,不 trim、不编码、不截断。必须是非空有效 UTF-8,最多 196 字节;topic key 中独立的 `*`、`#` 段禁止。完整 AMQP routing key 和 queue 名称不得超过 255 字节。 +- 输入 routing key 必须精确匹配 `.in`;输出 routing key 必须精确匹配 `.out`。每个租户独立绑定,不使用通配绑定替代身份隔离。 +- 当前 broker 只允许向 `agent-call.saas.v2` 发布;发布前要求该租户的 `.out` route 已声明。SaaS 队列不存在或消息未路由时,发布不能算成功。 + +### 投递与确认 + +- 发布使用 `application/json`、persistent delivery、mandatory routing,并等待 publisher confirm;mandatory return、负确认、超时或连接异常均视为未确认交付。 +- Dispatcher 先将事件/响应写入 SQLite outbox;只有收到正向 publisher confirm 且未发生 return 后才标记为已发布。重试沿用原消息身份和内容。 +- publisher confirm 只表示 RabbitMQ 接受了消息,不表示 SaaS 业务已消费或处理。 +- SaaS→D 使用 durable inbox 和手动 ACK。处理成功并完成持久化后 ACK;暂时错误 requeue;永久错误 reject,由 inbox 的 dead-letter 配置送入该租户 DLQ。 +- 默认 prefetch 为 1。单条消息必须是 UTF-8 JSON,大小为 1–262144 字节。 diff --git a/docs/contracts/mq-only-v1-freeze-proposal.md b/docs/thirds/mq-only-v1-freeze-proposal.md similarity index 100% rename from docs/contracts/mq-only-v1-freeze-proposal.md rename to docs/thirds/mq-only-v1-freeze-proposal.md diff --git a/docs/contracts/saas-dispatcher.md b/docs/thirds/saas-dispatcher.md similarity index 100% rename from docs/contracts/saas-dispatcher.md rename to docs/thirds/saas-dispatcher.md diff --git a/docs/thirds/saas-mq.md b/docs/thirds/saas-mq.md new file mode 100644 index 0000000..41909c1 --- /dev/null +++ b/docs/thirds/saas-mq.md @@ -0,0 +1,176 @@ +# SaaS → MQ → Dispatcher 消息定义 + +本文定义 SaaS 发往 RabbitMQ、由 Dispatcher 消费的当前消息结构。消息使用 UTF-8 JSON,`schema_version` 固定为 `2.0`;封闭的信封和 payload 拒绝未知字段,未定义的消息类型拒绝处理。明确开放的区域只有 `call.execute.variables`、AI 配置 `metadata` 及 AI 快照外层的扩展字段。单条消息体上限为 256 KiB。命令和 Service 信封中的普通 ID 为非空 JSON string,最多 128 个字符,且不得含空白、`/` 或 `\\`;时间为 RFC3339 JSON string。`dispatcher_id`、`tenant_key` 使用各自规则。 + +## 消息类型 + +| 顶层类型 | `command_type` / `message_type` | Dispatcher 处理 | +| --- | --- | --- | +| 命令 | `call.execute` | 创建并接收外呼任务 | +| 命令 | `task.control` | 暂停、恢复或停止任务 | +| 命令 | `call.replay` | 重发指定通话的已持久化业务事件 | +| 命令 | `command.replay` | 根据源命令重发已持久化业务事件 | +| Service request | `command.query` | 查询已接收命令状态,回复 `command.query.result` | +| Service request | `call.query` | 查询通话快照,回复 `call.query.result` | +| Service response | `ai.config.result` | 接收与原请求绑定的不可变 AI 配置快照及授权 | + +所有 SaaS→Dispatcher 消息都必须发往目标 Dispatcher 和租户的精确 `.in` routing key;交换机、队列和 routing key 见 [`dispatcher-mq.md`](dispatcher-mq.md)。 + +## 命令信封 + +```json +{ + "schema_version": "2.0", + "command_type": "call.execute", + "command_id": "", + "dispatcher_id": "", + "tenant_id": "", + "tenant_key": "", + "trace_id": "", + "issued_at": "", + "not_after": "", + "payload": {} +} +``` + +| 字段 | 类型 / 规则 | +| --- | --- | +| `schema_version` | 固定 `2.0` | +| `command_type` | `call.execute`、`task.control`、`call.replay`、`command.replay` 之一 | +| `command_id`、`tenant_id`、`trace_id` | 非空 ID,最多 128 个字符;不得含空白、`/` 或 `\\` | +| `dispatcher_id` | 规范小写 UUID v4;必须与目标 Dispatcher 一致 | +| `tenant_key` | 原值透传;非空有效 UTF-8,最多 196 字节;不得含独立的 `*` 或 `#` topic 段 | +| `issued_at`、`not_after` | RFC3339 时间。`now >= not_after` 时消息过期,不执行 | +| `payload` | 随 `command_type` 使用下列唯一结构;不接受额外字段 | + +### `call.execute` + +以下字段全部必填: + +| 字段 | 类型 / 规则 | +| --- | --- | +| `execution_id`、`task_id`、`task_item_id` | ID | +| `task_revision` | 整数,`>= 1` | +| `callee` | 非空字符串,最多 256 字符;Dispatcher 保留原值 | +| `route_policy_id`、`caller_profile_id`、`agent_version_id` | ID | +| `variables` | JSON object;业务变量字段由任务内容决定 | +| `ring_timeout_ms`、`max_call_duration_ms` | 整数,`>= 1` | + +### `task.control` + +| 字段 | 必填 | 类型 / 规则 | +| --- | --- | --- | +| `task_id` | 是 | ID | +| `action` | 是 | `pause`、`resume`、`stop` | +| `expected_task_revision` | 是 | 整数,`>= 1`;按该版本做 CAS 校验 | +| `reason` | 是 | 非空字符串,最多 512 字符 | +| `active_call_policy` | 否 | `drain` 或 `hangup` | + +### `call.replay` + +- `call_id`:必填 ID。 +- `reason`:必填非空字符串,最多 512 字符。 + +### `command.replay` + +- `source_command_id`:必填 ID。 +- `reason`:必填非空字符串,最多 512 字符。 + +重放只恢复原业务事件的交付,不创建新的执行、通话或录音资产,也不改变原事件身份和消息体。 + +## Service request 信封 + +`command.query` 和 `call.query` 共用以下信封,所有字段必填: + +```json +{ + "schema_version": "2.0", + "message_type": "command.query", + "message_id": "", + "dispatcher_id": "", + "tenant_id": "", + "tenant_key": "", + "trace_id": "", + "issued_at": "", + "not_after": "", + "payload": {} +} +``` + +| `message_type` | `payload` 必填字段 | +| --- | --- | +| `command.query` | `command_id`:ID | +| `call.query` | `call_id`:ID | + +Dispatcher 回复的 `*.result` 信封和 payload 定义见 [`dispatcher-mq.md`](dispatcher-mq.md)。相同 `message_id` 的重复请求必须保持消息体一致;相同 ID 不同内容会被拒绝。查询响应先持久化后确认输入消息。 + +## `ai.config.result` + +这是对 Dispatcher 原始 `ai.config.request` 的响应。所有字段必填;该响应信封不含 `not_after`: + +```json +{ + "schema_version": "2.0", + "message_type": "ai.config.result", + "message_id": "", + "dispatcher_id": "", + "tenant_id": "", + "tenant_key": "", + "trace_id": "", + "issued_at": "", + "correlation_id": "", + "status": "ok", + "reason_code": "ok", + "payload": {} +} +``` + +- `status`:`ok`、`pending`、`rejected`。 +- `correlation_id`:必须等于原 `ai.config.request.message_id`;`dispatcher_id`、`tenant_id`、`tenant_key` 也必须与原请求一致。 +- `pending`:`reason_code` 固定 `waiting`,`payload` 必须为空 object。 +- `rejected`:`reason_code` 为 `invalid_request`、`not_found`、`conflict`、`expired`、`not_authorized`、`unavailable`、`unsupported` 之一;`payload` 必须为 `{ "detail": "...", "retryable": true|false }`。 +- `ok`:`reason_code` 固定 `ok`,payload 必须同时包含 `snapshot` 和 `authorization`。 + +### `ok` 的 `snapshot` + +| 字段 | 类型 / 规则 | +| --- | --- | +| `tenant_id` | 与信封租户一致 | +| `agent_version_id` | Agent 配置版本 ID,须与原请求一致 | +| `status` | `published` 或 `reused` | +| `immutable` | 固定 `true` | +| `content_sha256` | 小写 64 位 SHA-256;须与配置规范化摘要一致 | +| `config` | 下述 AI 配置对象 | + +`config` 必填 `agent_version_id`、`immutable`、`asr`、`conversation`;`immutable` 固定 `true`。支持两种配置形态: + +- Full AI:必填 `llm`、`prompt`、`tts`,且 `conversation.opening` 必填;若提供 `mode`,值为 `full_ai`。 +- ASR-only:`mode` 必填且为 `asr_only`;不得提供 `llm`、`prompt`、`tts`。 + +| 对象 | 必填字段 | 可选字段与约束 | +| --- | --- | --- | +| `asr` | `provider_ref`、`language`、`input` | `credential_ref`、`model`、`interim`、`timeout_ms`;`input` 必须含 `encoding=pcm_s16le`、`sample_rate_hz`(8000–48000)、`channels=1`、`sample_width_bytes=2` | +| `llm` | `provider_ref`、`model` | `credential_ref`、`temperature`(0–2)、`max_tokens`(正整数)、`timeout_ms`(正整数) | +| `prompt` | `text`、`allowed_variables` | `text` 非空且最多 32768 字符;变量最多 32 个,名称匹配 `[A-Za-z_][A-Za-z0-9_]*`;`max_bytes` 为 1–32768 | +| `tts` | `provider_ref`、`model`、`voice`、`format` | `credential_ref`、`speed`(0.25–3)、`timeout_ms`;`format.encoding` 为 `pcm_s16le` 或 `pcma`,`sample_rate_hz` 为 8000–48000,`channels=1` | +| `conversation` | `allow_interrupt`、`silence_timeout_ms`、`max_duration_ms`、`max_turns`、`sentence_max_chars`、`max_pending_audio_chunks` | `opening`;`max_duration_ms` 为 1–3600000,`max_turns` 为 1–1000,其余整数下限为 1 | +| `metadata` | — | 可选 JSON object;不是业务参数透传通道 | + +除明确开放的 `metadata` object 外,各 AI 配置对象均拒绝未定义字段;AI 快照外层允许扩展字段,但这些字段不构成业务参数。`provider_ref`、`credential_ref` 是受控引用,不是凭据本身;MQ 消息不得携带密钥或访问令牌。 + +### `ok` 的 `authorization` + +必填:`authorization_id`、`tenant_id`、`tenant_key`、`agent_version_id`、`config_sha256`、`mode`、`issued_at`、`expires_at`、`source`、`revoked`。 + +- `mode`:`full_ai` 或 `asr_only`;`source`:`saas` 或 `mock-saas`。 +- `config_sha256`:小写 64 位 SHA-256;须与快照配置摘要一致。 +- `credential_refs` 可选,包含 `asr`、`llm`、`tts` 的受控引用。 +- `allowed_egress_pool_ids` 可选;提供时至少一个且不得重复。 +- `revoked=true` 时必须提供 `revocation_reason`(最多 256 字符)。 +- 授权中的租户、版本、配置摘要、有效期与快照及原请求必须匹配;拒绝已撤销或超出请求时间窗的响应。 + +目前 Dispatcher 已接入 `ai.config.result` 的消费和校验;正常运行链路尚无调用方发出 `ai.config.request`,因此两者当前不构成已接通的生产往返流程。 + +## 消费与重复投递 + +输入消息由 Dispatcher 按消息身份及租户范围校验。业务状态和所需响应/事件先持久化,处理成功后才 ACK;暂时性错误重入队,永久错误进入对应死信队列。重复消息只能恢复原身份的处理结果,不能重复创建拨号或业务资产。 diff --git a/docs/contracts/saas-rabbitmq-oss-dispatcher-agent-timeline.md b/docs/thirds/saas-rabbitmq-oss-dispatcher-agent-timeline.md similarity index 100% rename from docs/contracts/saas-rabbitmq-oss-dispatcher-agent-timeline.md rename to docs/thirds/saas-rabbitmq-oss-dispatcher-agent-timeline.md