@mebular/fleet 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (178) hide show
  1. package/dist/agentdir.d.ts +43 -0
  2. package/dist/agentdir.d.ts.map +1 -0
  3. package/dist/agentdir.js +107 -0
  4. package/dist/agentdir.js.map +1 -0
  5. package/dist/cli.d.ts +3 -0
  6. package/dist/cli.d.ts.map +1 -0
  7. package/dist/cli.js +1135 -0
  8. package/dist/cli.js.map +1 -0
  9. package/dist/collab/chatter.d.ts +54 -0
  10. package/dist/collab/chatter.d.ts.map +1 -0
  11. package/dist/collab/chatter.js +94 -0
  12. package/dist/collab/chatter.js.map +1 -0
  13. package/dist/collab/dag.d.ts +27 -0
  14. package/dist/collab/dag.d.ts.map +1 -0
  15. package/dist/collab/dag.js +107 -0
  16. package/dist/collab/dag.js.map +1 -0
  17. package/dist/collab/negotiation.d.ts +60 -0
  18. package/dist/collab/negotiation.d.ts.map +1 -0
  19. package/dist/collab/negotiation.js +90 -0
  20. package/dist/collab/negotiation.js.map +1 -0
  21. package/dist/collab/tree.d.ts +39 -0
  22. package/dist/collab/tree.d.ts.map +1 -0
  23. package/dist/collab/tree.js +177 -0
  24. package/dist/collab/tree.js.map +1 -0
  25. package/dist/config.d.ts +123 -0
  26. package/dist/config.d.ts.map +1 -0
  27. package/dist/config.js +206 -0
  28. package/dist/config.js.map +1 -0
  29. package/dist/index.d.ts +33 -0
  30. package/dist/index.d.ts.map +1 -0
  31. package/dist/index.js +40 -0
  32. package/dist/index.js.map +1 -0
  33. package/dist/join.d.ts +50 -0
  34. package/dist/join.d.ts.map +1 -0
  35. package/dist/join.js +167 -0
  36. package/dist/join.js.map +1 -0
  37. package/dist/jointoken.d.ts +144 -0
  38. package/dist/jointoken.d.ts.map +1 -0
  39. package/dist/jointoken.js +472 -0
  40. package/dist/jointoken.js.map +1 -0
  41. package/dist/model.d.ts +64 -0
  42. package/dist/model.d.ts.map +1 -0
  43. package/dist/model.js +158 -0
  44. package/dist/model.js.map +1 -0
  45. package/dist/onboard.d.ts +90 -0
  46. package/dist/onboard.d.ts.map +1 -0
  47. package/dist/onboard.js +510 -0
  48. package/dist/onboard.js.map +1 -0
  49. package/dist/protocol/envelope.d.ts +94 -0
  50. package/dist/protocol/envelope.d.ts.map +1 -0
  51. package/dist/protocol/envelope.js +169 -0
  52. package/dist/protocol/envelope.js.map +1 -0
  53. package/dist/protocol/events.d.ts +51 -0
  54. package/dist/protocol/events.d.ts.map +1 -0
  55. package/dist/protocol/events.js +87 -0
  56. package/dist/protocol/events.js.map +1 -0
  57. package/dist/qr.d.ts +15 -0
  58. package/dist/qr.d.ts.map +1 -0
  59. package/dist/qr.js +52 -0
  60. package/dist/qr.js.map +1 -0
  61. package/dist/quickstart.d.ts +167 -0
  62. package/dist/quickstart.d.ts.map +1 -0
  63. package/dist/quickstart.js +482 -0
  64. package/dist/quickstart.js.map +1 -0
  65. package/dist/quota.d.ts +41 -0
  66. package/dist/quota.d.ts.map +1 -0
  67. package/dist/quota.js +84 -0
  68. package/dist/quota.js.map +1 -0
  69. package/dist/runtime/agent.d.ts +100 -0
  70. package/dist/runtime/agent.d.ts.map +1 -0
  71. package/dist/runtime/agent.js +260 -0
  72. package/dist/runtime/agent.js.map +1 -0
  73. package/dist/runtime/executor.d.ts +42 -0
  74. package/dist/runtime/executor.d.ts.map +1 -0
  75. package/dist/runtime/executor.js +87 -0
  76. package/dist/runtime/executor.js.map +1 -0
  77. package/dist/runtime/fairness.d.ts +25 -0
  78. package/dist/runtime/fairness.d.ts.map +1 -0
  79. package/dist/runtime/fairness.js +79 -0
  80. package/dist/runtime/fairness.js.map +1 -0
  81. package/dist/runtime/node.d.ts +91 -0
  82. package/dist/runtime/node.d.ts.map +1 -0
  83. package/dist/runtime/node.js +203 -0
  84. package/dist/runtime/node.js.map +1 -0
  85. package/dist/runtime/openchamber-http.d.ts +27 -0
  86. package/dist/runtime/openchamber-http.d.ts.map +1 -0
  87. package/dist/runtime/openchamber-http.js +118 -0
  88. package/dist/runtime/openchamber-http.js.map +1 -0
  89. package/dist/runtime/openchamber.d.ts +38 -0
  90. package/dist/runtime/openchamber.d.ts.map +1 -0
  91. package/dist/runtime/openchamber.js +43 -0
  92. package/dist/runtime/openchamber.js.map +1 -0
  93. package/dist/runtime/planner.d.ts +23 -0
  94. package/dist/runtime/planner.d.ts.map +1 -0
  95. package/dist/runtime/planner.js +17 -0
  96. package/dist/runtime/planner.js.map +1 -0
  97. package/dist/runtime/worker.d.ts +96 -0
  98. package/dist/runtime/worker.d.ts.map +1 -0
  99. package/dist/runtime/worker.js +296 -0
  100. package/dist/runtime/worker.js.map +1 -0
  101. package/dist/security.d.ts +462 -0
  102. package/dist/security.d.ts.map +1 -0
  103. package/dist/security.js +1197 -0
  104. package/dist/security.js.map +1 -0
  105. package/dist/store/daemon-store.d.ts +33 -0
  106. package/dist/store/daemon-store.d.ts.map +1 -0
  107. package/dist/store/daemon-store.js +115 -0
  108. package/dist/store/daemon-store.js.map +1 -0
  109. package/dist/store/file-store.d.ts +22 -0
  110. package/dist/store/file-store.d.ts.map +1 -0
  111. package/dist/store/file-store.js +63 -0
  112. package/dist/store/file-store.js.map +1 -0
  113. package/dist/store/mebular-store.d.ts +22 -0
  114. package/dist/store/mebular-store.d.ts.map +1 -0
  115. package/dist/store/mebular-store.js +53 -0
  116. package/dist/store/mebular-store.js.map +1 -0
  117. package/dist/store/message-store.d.ts +26 -0
  118. package/dist/store/message-store.d.ts.map +1 -0
  119. package/dist/store/message-store.js +51 -0
  120. package/dist/store/message-store.js.map +1 -0
  121. package/dist/surface.d.ts +43 -0
  122. package/dist/surface.d.ts.map +1 -0
  123. package/dist/surface.js +515 -0
  124. package/dist/surface.js.map +1 -0
  125. package/dist/transport/null.d.ts +7 -0
  126. package/dist/transport/null.d.ts.map +1 -0
  127. package/dist/transport/null.js +16 -0
  128. package/dist/transport/null.js.map +1 -0
  129. package/dist/transport/spool.d.ts +12 -0
  130. package/dist/transport/spool.d.ts.map +1 -0
  131. package/dist/transport/spool.js +54 -0
  132. package/dist/transport/spool.js.map +1 -0
  133. package/dist/transport/types.d.ts +21 -0
  134. package/dist/transport/types.d.ts.map +1 -0
  135. package/dist/transport/types.js +6 -0
  136. package/dist/transport/types.js.map +1 -0
  137. package/dist/version.json +6 -0
  138. package/package.json +37 -4
  139. package/protocol/collab.example.json +23 -0
  140. package/protocol/daemon-home.example.json +66 -0
  141. package/protocol/envelope.example.json +12 -0
  142. package/protocol/state-machine.json +14 -0
  143. package/protocol/task-tree.example.json +72 -0
  144. package/src/agentdir.ts +138 -0
  145. package/src/cli.ts +1192 -0
  146. package/src/collab/chatter.ts +116 -0
  147. package/src/collab/dag.ts +124 -0
  148. package/src/collab/negotiation.ts +130 -0
  149. package/src/collab/tree.ts +184 -0
  150. package/src/config.ts +286 -0
  151. package/src/index.ts +39 -0
  152. package/src/join.ts +227 -0
  153. package/src/jointoken.ts +609 -0
  154. package/src/model.ts +203 -0
  155. package/src/onboard.ts +601 -0
  156. package/src/protocol/envelope.ts +228 -0
  157. package/src/protocol/events.ts +135 -0
  158. package/src/qr.ts +68 -0
  159. package/src/quickstart.ts +632 -0
  160. package/src/quota.ts +112 -0
  161. package/src/runtime/agent.ts +313 -0
  162. package/src/runtime/executor.ts +114 -0
  163. package/src/runtime/fairness.ts +90 -0
  164. package/src/runtime/node.ts +250 -0
  165. package/src/runtime/openchamber-http.ts +137 -0
  166. package/src/runtime/openchamber.ts +66 -0
  167. package/src/runtime/planner.ts +36 -0
  168. package/src/runtime/worker.ts +354 -0
  169. package/src/security.ts +1660 -0
  170. package/src/store/daemon-store.ts +144 -0
  171. package/src/store/file-store.ts +75 -0
  172. package/src/store/mebular-store.ts +66 -0
  173. package/src/store/message-store.ts +62 -0
  174. package/src/surface.ts +535 -0
  175. package/src/transport/null.ts +20 -0
  176. package/src/transport/spool.ts +62 -0
  177. package/src/transport/types.ts +27 -0
  178. package/README.md +0 -3
package/src/surface.ts ADDED
@@ -0,0 +1,535 @@
1
+ // W1 Agent 任务面:**单一工具注册表**(MCP 与 CLI 共用同一 handler,能力完全一致)。
2
+ //
3
+ // 域 = 记忆数据通道(数据义务);任务 = 树/DAG;派发三层 L1 传输授权 / L2 任务树 / L3 本地执行。
4
+ // 本模块只消费 `@mebular/core` 公共 API;工具输出为**结构化 JSON**。
5
+
6
+ import { Mebular } from '@mebular/core';
7
+ import {
8
+ loadToolConfig,
9
+ readMasterKeyFile,
10
+ } from './config.js';
11
+ import { declareNamespace, offlineMebularOptions, subscribeNamespace } from './onboard.js';
12
+ import { MebularTaskEventStore } from './store/mebular-store.js';
13
+ import { negotiationMessageStore, type NegotiationMessage } from './collab/negotiation.js';
14
+ import { chatterMessageStore, FleetChatter } from './collab/chatter.js';
15
+ import { FleetNode } from './runtime/node.js';
16
+ import { NullTransport } from './transport/null.js';
17
+ import { LocalQuota } from './quota.js';
18
+ import { deriveEdges } from './collab/dag.js';
19
+ import { reduceTaskEvents, type TaskState } from './model.js';
20
+ import { agentDirectoryStore, expandTargets } from './agentdir.js';
21
+ import type { TaskEvent, TaskEventType } from './protocol/events.js';
22
+ import { EVENT_TO_STATUS } from './protocol/events.js';
23
+ import { DEFAULT_ROOT_BUDGET, type TaskBudget, type TaskDispatch } from './protocol/envelope.js';
24
+
25
+ export interface ToolContext {
26
+ dir: string;
27
+ namespace?: string;
28
+ agent?: string;
29
+ /**
30
+ * 运行中 daemon 的 live app(由 MCP server 注入)。
31
+ *
32
+ * 提供时 **复用它**:不 `initialize`、不 `shutdown`、不新建第二个离线 Mebular。
33
+ * 这样任务事件写入的是 daemon 自己的 EventLog —— 本地 append / push-on-write / SSE 都能感知;
34
+ * 若另开离线实例写同一 JSONL,daemon 的 EventLog 不感知 append,远端与 SSE 都收不到。
35
+ *
36
+ * 缺省(CLI / stdio 离线路径):维持原有行为,按 `dir` 新建并关闭一个离线实例。
37
+ */
38
+ mebular?: Mebular;
39
+ }
40
+
41
+ export interface TaskTool {
42
+ name: string;
43
+ /** 等价 CLI 子命令(`fleet <cli>`) */
44
+ cli: string;
45
+ /**
46
+ * R1.3 / 评审 H1:OAuth scope **逐工具显式声明**(禁止按名字推断——`board_create` 曾因归入
47
+ * 只读组而被 `task.read`-only 令牌越权调用)。两轴:task.read(只读)/ task.write(写与授权)。
48
+ */
49
+ scope: 'task.read' | 'task.write';
50
+ description: string;
51
+ inputSchema: Record<string, unknown>;
52
+ handler: (input: Record<string, unknown>, ctx: ToolContext) => Promise<unknown>;
53
+ }
54
+
55
+ interface Session {
56
+ mebular: Mebular;
57
+ store: MebularTaskEventStore;
58
+ node: FleetNode;
59
+ device: string;
60
+ agent: string;
61
+ namespace: string;
62
+ close: () => Promise<void>;
63
+ }
64
+
65
+ async function openSession(ctx: ToolContext): Promise<Session> {
66
+ // F-UNI:fleet home(fleet.config.json)或守护 home(config.json)都可作为工具上下文
67
+ const config = await loadToolConfig(ctx.dir);
68
+ const namespace = ctx.namespace ?? config.namespace;
69
+ const agent = ctx.agent ?? 'board';
70
+ const live = ctx.mebular;
71
+ let mebular: Mebular;
72
+ if (live !== undefined) {
73
+ // 运行中 daemon:复用 live app(同一实例 / 同一 EventLog)——写入即本地 append,推送与 SSE 正常。
74
+ mebular = live;
75
+ } else {
76
+ const encryption = await readMasterKeyFile(config.masterKeyFile);
77
+ mebular = new Mebular(offlineMebularOptions(config, encryption) as never);
78
+ await mebular.initialize();
79
+ }
80
+ const ownsMebular = live === undefined;
81
+ const device = live?.deviceId ?? config.device;
82
+ const store = new MebularTaskEventStore(mebular, { namespace });
83
+ const node = new FleetNode({
84
+ device,
85
+ agent,
86
+ store,
87
+ transport: new NullTransport(),
88
+ quota: new LocalQuota({ limitPerDevice: config.quotaLimitPerDevice ?? 1_000_000 }),
89
+ });
90
+ return {
91
+ mebular,
92
+ store,
93
+ node,
94
+ device,
95
+ agent,
96
+ namespace,
97
+ close: async () => {
98
+ // live app 的生命周期归 daemon(MCP serve)所有:工具调用只结束会话,不关闭它。
99
+ if (ownsMebular) await mebular.shutdown();
100
+ },
101
+ };
102
+ }
103
+
104
+ function strInput(input: Record<string, unknown>, key: string): string | undefined {
105
+ const v = input[key];
106
+ return typeof v === 'string' && v.length > 0 ? v : undefined;
107
+ }
108
+ function requiredString(input: Record<string, unknown>, key: string): string {
109
+ const v = strInput(input, key);
110
+ if (v === undefined) throw new Error(`${key} 必填(非空字符串)`);
111
+ return v;
112
+ }
113
+
114
+ async function statesOf(store: MebularTaskEventStore): Promise<TaskState[]> {
115
+ const byTask = new Map<string, TaskEvent[]>();
116
+ for (const e of await store.all()) {
117
+ const list = byTask.get(e.taskId);
118
+ if (list) list.push(e);
119
+ else byTask.set(e.taskId, [e]);
120
+ }
121
+ return [...byTask.keys()].sort().map((id) => reduceTaskEvents(byTask.get(id)!)).filter((s): s is TaskState => s !== null);
122
+ }
123
+
124
+ function summarizeState(s: TaskState): Record<string, unknown> {
125
+ return {
126
+ taskId: s.taskId,
127
+ status: s.status,
128
+ from: s.from,
129
+ to: s.to,
130
+ intent: s.intent,
131
+ attempts: s.attempts,
132
+ trace: s.trace,
133
+ lastEventId: s.lastEventId,
134
+ ...(s.resultRef !== undefined ? { resultRef: s.resultRef } : {}),
135
+ ...(s.reason !== undefined ? { reason: s.reason } : {}),
136
+ ...(s.budget !== undefined ? { budget: s.budget } : {}),
137
+ ...(s.dispatch !== undefined ? { dispatch: s.dispatch } : {}),
138
+ };
139
+ }
140
+
141
+ function budgetInput(input: Record<string, unknown>): TaskBudget | undefined {
142
+ const b = input.budget;
143
+ if (b === undefined) return undefined;
144
+ if (typeof b !== 'object' || b === null) throw new Error('budget 必须为对象');
145
+ const r = b as Record<string, unknown>;
146
+ const norm = (k: string, d: number): number => (r[k] === undefined ? d : Number(r[k]));
147
+ const budget: TaskBudget = { maxDepth: norm('maxDepth', DEFAULT_ROOT_BUDGET.maxDepth), maxChildren: norm('maxChildren', DEFAULT_ROOT_BUDGET.maxChildren), maxTasks: norm('maxTasks', DEFAULT_ROOT_BUDGET.maxTasks) };
148
+ for (const [k, v] of Object.entries(budget)) {
149
+ if (!Number.isInteger(v) || v < 0) throw new Error(`budget.${k} 必须为非负整数`);
150
+ }
151
+ return budget;
152
+ }
153
+ function dispatchInput(input: Record<string, unknown>): TaskDispatch | undefined {
154
+ const d = input.dispatch;
155
+ if (d === undefined) return undefined;
156
+ if (d !== 'children-ok' && d !== 'root-only') throw new Error('dispatch 必须为 children-ok|root-only');
157
+ return d;
158
+ }
159
+
160
+ export const TASK_TOOLS: TaskTool[] = [
161
+ {
162
+ name: 'task_submit',
163
+ cli: 'task_submit',
164
+ scope: 'task.write',
165
+ description: '提交任务(root 或 child);root 可带预算与派发策略。**非幂等**:重试 = 新任务(需要关联请用 causedBy/chain)',
166
+ inputSchema: {
167
+ type: 'object',
168
+ required: ['intent', 'to'],
169
+ properties: {
170
+ intent: { type: 'string' },
171
+ to: { type: 'object', properties: { device: { type: 'string' }, agent: { type: 'string' } } },
172
+ payloadRef: { type: 'string' },
173
+ expiresAt: { type: 'number' },
174
+ causedBy: { type: 'string' },
175
+ chain: { type: 'array', items: { type: 'string' } },
176
+ budget: { type: 'object' },
177
+ dispatch: { type: 'string', enum: ['children-ok', 'root-only'] },
178
+ },
179
+ },
180
+ handler: async (input, ctx) => {
181
+ const session = await openSession(ctx);
182
+ try {
183
+ const to = (input.to ?? {}) as { device?: string; agent?: string };
184
+ const causedBy = strInput(input, 'causedBy');
185
+ const chain = Array.isArray(input.chain) ? (input.chain as string[]) : [];
186
+ const budget = budgetInput(input);
187
+ const dispatch = dispatchInput(input);
188
+ const result = await session.node.submit({
189
+ intent: requiredString(input, 'intent'),
190
+ to: { device: requiredString(to as Record<string, unknown>, 'device'), agent: requiredString(to as Record<string, unknown>, 'agent') },
191
+ ...(strInput(input, 'payloadRef') !== undefined ? { payloadRef: strInput(input, 'payloadRef')! } : {}),
192
+ ...(typeof input.expiresAt === 'number' ? { expiresAt: input.expiresAt } : {}),
193
+ ...(causedBy !== undefined ? { trace: { causedBy, chain: [...chain, causedBy] } } : { trace: { chain } }),
194
+ ...(budget !== undefined ? { budget } : {}),
195
+ ...(dispatch !== undefined ? { dispatch } : {}),
196
+ });
197
+ const state = result.taskId !== null ? reduceTaskEvents(await session.store.byTask(result.taskId)) : null;
198
+ return { ok: result.taskId !== null, taskId: result.taskId, decision: result.decision, from: { device: session.device, agent: session.agent }, ...(state ? { state: summarizeState(state) } : {}) };
199
+ } finally {
200
+ await session.close();
201
+ }
202
+ },
203
+ },
204
+ {
205
+ name: 'task_submit_batch',
206
+ cli: 'task_submit_batch',
207
+ scope: 'task.write',
208
+ description: '批量提交任务(同 root 语义)',
209
+ inputSchema: { type: 'object', required: ['tasks'], properties: { tasks: { type: 'array' } } },
210
+ handler: async (input, ctx) => {
211
+ const tasks = input.tasks;
212
+ if (!Array.isArray(tasks) || tasks.length === 0) throw new Error('tasks 必须为非空数组');
213
+ const results = [];
214
+ for (const t of tasks as Array<Record<string, unknown>>) {
215
+ results.push(await TASK_TOOLS.find((x) => x.name === 'task_submit')!.handler(t, ctx));
216
+ }
217
+ return { ok: results.every((r) => (r as { ok: boolean }).ok), count: results.length, results };
218
+ },
219
+ },
220
+ {
221
+ name: 'task_cancel',
222
+ cli: 'task_cancel',
223
+ scope: 'task.write',
224
+ description: '取消未终态任务(写 failed,reason=CANCELLED)',
225
+ inputSchema: { type: 'object', required: ['taskId'], properties: { taskId: { type: 'string' }, reason: { type: 'string' } } },
226
+ handler: async (input, ctx) => {
227
+ const session = await openSession(ctx);
228
+ try {
229
+ const taskId = requiredString(input, 'taskId');
230
+ const state = reduceTaskEvents(await session.store.byTask(taskId));
231
+ if (state === null) throw new Error(`任务不存在:${taskId}`);
232
+ if (state.terminal) return { ok: false, taskId, reason: 'already-terminal', status: state.status };
233
+ const reason = strInput(input, 'reason') ?? 'CANCELLED';
234
+ const event: TaskEvent = {
235
+ v: 1,
236
+ eventId: `${taskId}#failed@${session.device}`,
237
+ taskId,
238
+ type: 'failed',
239
+ actor: { device: session.device, agent: session.agent },
240
+ at: Date.now(),
241
+ toStatus: 'failed',
242
+ trace: state.trace,
243
+ reason,
244
+ };
245
+ await session.store.append(event);
246
+ return { ok: true, taskId, status: 'failed', reason };
247
+ } finally {
248
+ await session.close();
249
+ }
250
+ },
251
+ },
252
+ {
253
+ name: 'task_retry',
254
+ cli: 'task_retry',
255
+ scope: 'task.write',
256
+ description: '以新任务重试(链表达:新任务 causedBy 原任务)',
257
+ inputSchema: { type: 'object', required: ['taskId'], properties: { taskId: { type: 'string' } } },
258
+ handler: async (input, ctx) => {
259
+ const session = await openSession(ctx);
260
+ try {
261
+ const taskId = requiredString(input, 'taskId');
262
+ const state = reduceTaskEvents(await session.store.byTask(taskId));
263
+ if (state === null) throw new Error(`任务不存在:${taskId}`);
264
+ const result = await session.node.submit({
265
+ intent: state.intent,
266
+ to: state.to,
267
+ ...(state.payloadRef !== undefined ? { payloadRef: state.payloadRef } : {}),
268
+ trace: { causedBy: taskId, chain: [...state.trace.chain, taskId] },
269
+ });
270
+ return { ok: result.taskId !== null, taskId: result.taskId, retryOf: taskId, decision: result.decision };
271
+ } finally {
272
+ await session.close();
273
+ }
274
+ },
275
+ },
276
+ {
277
+ name: 'task_status',
278
+ cli: 'task_status',
279
+ scope: 'task.read',
280
+ description: '查询单任务权威状态',
281
+ inputSchema: { type: 'object', required: ['taskId'], properties: { taskId: { type: 'string' } } },
282
+ handler: async (input, ctx) => {
283
+ const session = await openSession(ctx);
284
+ try {
285
+ const taskId = requiredString(input, 'taskId');
286
+ const state = reduceTaskEvents(await session.store.byTask(taskId));
287
+ return state === null ? { ok: false, taskId, state: null } : { ok: true, taskId, state: summarizeState(state) };
288
+ } finally {
289
+ await session.close();
290
+ }
291
+ },
292
+ },
293
+ {
294
+ name: 'task_list',
295
+ cli: 'task_list',
296
+ scope: 'task.read',
297
+ description: '列出任务(可按 status/from 过滤)',
298
+ inputSchema: { type: 'object', properties: { status: { type: 'string' }, fromDevice: { type: 'string' } } },
299
+ handler: async (input, ctx) => {
300
+ const session = await openSession(ctx);
301
+ try {
302
+ let states = await statesOf(session.store);
303
+ const status = strInput(input, 'status');
304
+ const fromDevice = strInput(input, 'fromDevice');
305
+ if (status !== undefined) states = states.filter((s) => s.status === status);
306
+ if (fromDevice !== undefined) states = states.filter((s) => s.from.device === fromDevice);
307
+ return { ok: true, count: states.length, tasks: states.map(summarizeState) };
308
+ } finally {
309
+ await session.close();
310
+ }
311
+ },
312
+ },
313
+ {
314
+ name: 'task_history',
315
+ cli: 'task_history',
316
+ scope: 'task.read',
317
+ description: '任务的全部事件(确定序)',
318
+ inputSchema: { type: 'object', required: ['taskId'], properties: { taskId: { type: 'string' } } },
319
+ handler: async (input, ctx) => {
320
+ const session = await openSession(ctx);
321
+ try {
322
+ const taskId = requiredString(input, 'taskId');
323
+ const events = (await session.store.byTask(taskId)).sort((a, b) => (a.eventId < b.eventId ? -1 : 1));
324
+ return { ok: true, taskId, events };
325
+ } finally {
326
+ await session.close();
327
+ }
328
+ },
329
+ },
330
+ {
331
+ name: 'task_children',
332
+ cli: 'task_children',
333
+ scope: 'task.read',
334
+ description: '任务的直接子任务(trace.causedBy)',
335
+ inputSchema: { type: 'object', required: ['taskId'], properties: { taskId: { type: 'string' } } },
336
+ handler: async (input, ctx) => {
337
+ const session = await openSession(ctx);
338
+ try {
339
+ const taskId = requiredString(input, 'taskId');
340
+ const states = await statesOf(session.store);
341
+ const children = deriveEdges(states).filter((e) => e.parent === taskId).map((e) => e.child).sort();
342
+ return { ok: true, taskId, children };
343
+ } finally {
344
+ await session.close();
345
+ }
346
+ },
347
+ },
348
+ {
349
+ name: 'task_summarize',
350
+ cli: 'task_summarize',
351
+ scope: 'task.read',
352
+ description: '从 root 可达子图的汇总(复用 summarizeDag)',
353
+ inputSchema: { type: 'object', required: ['taskId'], properties: { taskId: { type: 'string' } } },
354
+ handler: async (input, ctx) => {
355
+ const session = await openSession(ctx);
356
+ try {
357
+ const taskId = requiredString(input, 'taskId');
358
+ return { ok: true, ...(await session.node.summarizeDag(taskId)) };
359
+ } finally {
360
+ await session.close();
361
+ }
362
+ },
363
+ },
364
+ {
365
+ name: 'task_subscribe',
366
+ cli: 'task_subscribe',
367
+ scope: 'task.write',
368
+ description: '变化订阅:返回自 cursor 起有变化的任务(单次);CLI --watch 轮询',
369
+ inputSchema: { type: 'object', properties: { cursor: { type: 'object' } } },
370
+ handler: async (input, ctx) => {
371
+ const session = await openSession(ctx);
372
+ try {
373
+ const cursor = (input.cursor ?? {}) as Record<string, string>;
374
+ const states = await statesOf(session.store);
375
+ const changed = states.filter((s) => cursor[s.taskId] !== s.lastEventId).map(summarizeState);
376
+ const nextCursor = Object.fromEntries(states.map((s) => [s.taskId, s.lastEventId]));
377
+ return { ok: true, changed, cursor: nextCursor };
378
+ } finally {
379
+ await session.close();
380
+ }
381
+ },
382
+ },
383
+ {
384
+ name: 'task_negotiate',
385
+ cli: 'task_negotiate',
386
+ scope: 'task.write',
387
+ description: '发送协商消息(clarify/counter/accept/reject;messageId 幂等)',
388
+ inputSchema: { type: 'object', required: ['taskId', 'kind', 'round'], properties: { taskId: { type: 'string' }, kind: { type: 'string' }, round: { type: 'number' }, text: { type: 'string' } } },
389
+ handler: async (input, ctx) => {
390
+ const session = await openSession(ctx);
391
+ try {
392
+ const taskId = requiredString(input, 'taskId');
393
+ const kind = requiredString(input, 'kind');
394
+ if (!['clarify', 'counter', 'accept', 'reject'].includes(kind)) throw new Error('kind 非法');
395
+ const round = Number(input.round);
396
+ if (!Number.isInteger(round) || round < 0) throw new Error('round 必须为非负整数');
397
+ const store = negotiationMessageStore(session.mebular, session.namespace);
398
+ const message: NegotiationMessage = {
399
+ v: 1,
400
+ messageId: `${taskId}#neg#${round}@${session.device}`,
401
+ taskId,
402
+ round,
403
+ from: { device: session.device, agent: session.agent },
404
+ kind: kind as NegotiationMessage['kind'],
405
+ ...(strInput(input, 'text') !== undefined ? { text: strInput(input, 'text')! } : {}),
406
+ };
407
+ const appended = await store.append(message);
408
+ return { ok: true, appended, messageId: message.messageId };
409
+ } finally {
410
+ await session.close();
411
+ }
412
+ },
413
+ },
414
+ {
415
+ name: 'chatter_send',
416
+ cli: 'chatter_send',
417
+ scope: 'task.write',
418
+ description: '配额制闲聊发送(本地配额记账)',
419
+ inputSchema: { type: 'object', required: ['topic', 'text'], properties: { topic: { type: 'string' }, text: { type: 'string' }, messageId: { type: 'string' } } },
420
+ handler: async (input, ctx) => {
421
+ const session = await openSession(ctx);
422
+ try {
423
+ const quota = new LocalQuota({ limitPerDevice: 1000 });
424
+ const chatter = new FleetChatter({ device: session.device, quota, store: chatterMessageStore(session.mebular, session.namespace) });
425
+ const messageId = strInput(input, 'messageId') ?? `${session.device}-${Date.now().toString(36)}`;
426
+ const decision = await chatter.send({
427
+ v: 1,
428
+ messageId,
429
+ from: { device: session.device, agent: session.agent },
430
+ topic: requiredString(input, 'topic'),
431
+ text: requiredString(input, 'text'),
432
+ });
433
+ return { ok: decision === 'accepted', decision, messageId };
434
+ } finally {
435
+ await session.close();
436
+ }
437
+ },
438
+ },
439
+ {
440
+ name: 'chatter_inbox',
441
+ cli: 'chatter_inbox',
442
+ scope: 'task.write',
443
+ description: '闲聊收件箱(幂等去重、确定序)',
444
+ inputSchema: { type: 'object', properties: {} },
445
+ handler: async (_input, ctx) => {
446
+ const session = await openSession(ctx);
447
+ try {
448
+ const chatter = new FleetChatter({ device: session.device, quota: new LocalQuota({ limitPerDevice: 1000 }), store: chatterMessageStore(session.mebular, session.namespace) });
449
+ const inbox = await chatter.inbox();
450
+ return { ok: true, count: inbox.length, messages: inbox };
451
+ } finally {
452
+ await session.close();
453
+ }
454
+ },
455
+ },
456
+ {
457
+ name: 'task_quota',
458
+ cli: 'task_quota',
459
+ scope: 'task.read',
460
+ description: '本机配额状态(每设备对自己发出本地记账;无全局账本)',
461
+ inputSchema: { type: 'object', properties: {} },
462
+ handler: async (_input, ctx) => {
463
+ const config = await loadToolConfig(ctx.dir);
464
+ return { ok: true, device: config.device, limitPerDevice: config.quotaLimitPerDevice ?? 1_000_000, note: '本地记账,无全局账本;墙钟不参与一致性判定' };
465
+ },
466
+ },
467
+ {
468
+ name: 'task_targets',
469
+ cli: 'task_targets',
470
+ scope: 'task.read',
471
+ description: '我能派给谁:订阅任务域的对端 ∩ 其 Agent 目录(device, agent)',
472
+ inputSchema: { type: 'object', properties: {} },
473
+ handler: async (_input, ctx) => {
474
+ const session = await openSession(ctx);
475
+ try {
476
+ const config = await loadToolConfig(ctx.dir);
477
+ const subscribed: string[] = [];
478
+ for (const peer of config.peers) {
479
+ const namespaces = await session.mebular.getSubscriptions(peer.device);
480
+ if (namespaces.includes(session.namespace)) subscribed.push(peer.device);
481
+ }
482
+ const entries = await agentDirectoryStore(session.mebular, 'agents').all();
483
+ const targets = expandTargets(entries, subscribed);
484
+ return { ok: true, namespace: session.namespace, subscribed, targets };
485
+ } finally {
486
+ await session.close();
487
+ }
488
+ },
489
+ },
490
+ {
491
+ name: 'board_create',
492
+ cli: 'board_create',
493
+ scope: 'task.write',
494
+ description: '建板(= 建域 + 本机订阅 + 邀请成员);域是板的薄封装',
495
+ inputSchema: { type: 'object', required: ['name'], properties: { name: { type: 'string' }, with: { type: 'array', items: { type: 'string' } } } },
496
+ handler: async (input, ctx) => {
497
+ const name = requiredString(input, 'name');
498
+ const withDevices = Array.isArray(input.with) ? (input.with as string[]) : [];
499
+ const config = await loadToolConfig(ctx.dir);
500
+ const live = ctx.mebular !== undefined ? { mebular: ctx.mebular } : {};
501
+ // 建域 = 公告(零门槛)+ 本机订阅;其他设备自行订阅(不能代订)。
502
+ // 运行中 daemon 注入 live app 时复用同一 EventLog(不另开离线写者)。
503
+ const declaration = await declareNamespace(ctx.dir, { namespace: name, note: `board ${name}`, ...live });
504
+ const subscription = await subscribeNamespace(ctx.dir, { namespace: name, note: `board ${name}`, ...live });
505
+ return {
506
+ ok: true,
507
+ namespace: name,
508
+ declaredBy: config.device,
509
+ declarationEventId: declaration.eventId,
510
+ subscribedBy: [config.device],
511
+ subscriptionEventId: subscription.eventId,
512
+ invited: withDevices,
513
+ note: '其他设备需自行订阅该域(订阅零门槛;不能替他人订阅)',
514
+ };
515
+ },
516
+ },
517
+ ];
518
+
519
+ /** 工具名 → handler(MCP tools/call 用)。 */
520
+ export function toolByName(name: string): TaskTool | undefined {
521
+ return TASK_TOOLS.find((t) => t.name === name);
522
+ }
523
+ /** CLI 子命令 → handler。 */
524
+ export function toolByCli(cli: string): TaskTool | undefined {
525
+ return TASK_TOOLS.find((t) => t.cli === cli);
526
+ }
527
+ /** 工具 ↔ CLI 对照表(文档/测试用)。 */
528
+ export function toolCliTable(): Array<{ tool: string; cli: string }> {
529
+ return TASK_TOOLS.map((t) => ({ tool: t.name, cli: t.cli }));
530
+ }
531
+
532
+ /** 事件类型 → 状态(供表面工具/测试复用,避免硬编码)。 */
533
+ export function statusOfEventType(type: TaskEventType): string {
534
+ return EVENT_TO_STATUS[type];
535
+ }
@@ -0,0 +1,20 @@
1
+ // 空传输(M3):当任务事件直接落在**会被同步复制的记忆**里时,无需额外投递通道。
2
+ //
3
+ // 语义:`publish` 为 no-op(事件已由 core 记忆同步传播);`drain` 恒空(消费方直接读本地存储,
4
+ // 该存储会随同步收敛)。用于 `MebularTaskEventStore`。
5
+
6
+ import type { FleetMessage, TaskTransport } from './types.js';
7
+
8
+ export class NullTransport implements TaskTransport {
9
+ async publish(): Promise<void> {
10
+ // no-op:投递由记忆同步负责。
11
+ }
12
+
13
+ async drain(): Promise<FleetMessage[]> {
14
+ return [];
15
+ }
16
+
17
+ async close(): Promise<void> {
18
+ // 无资源。
19
+ }
20
+ }
@@ -0,0 +1,62 @@
1
+ // 本地 spool 传输(M2):单机两进程用一个目录做收件箱,文件级投递。
2
+ //
3
+ // 目录布局:`<spool>/<device>/<msgId>.json`;消费后移入 `<spool>/<device>/.processed/`。
4
+ // 崩溃可能重投(至少一次),消费方按 `eventId` 幂等。
5
+
6
+ import { mkdir, readFile, readdir, rename, writeFile } from 'node:fs/promises';
7
+ import { dirname, join } from 'node:path';
8
+ import type { FleetEndpoint } from '../protocol/envelope.js';
9
+ import type { FleetMessage, TaskTransport } from './types.js';
10
+
11
+ export class SpoolTransport implements TaskTransport {
12
+ private readonly root: string;
13
+
14
+ constructor(spoolDir: string) {
15
+ this.root = spoolDir;
16
+ }
17
+
18
+ private inbox(device: string): string {
19
+ return join(this.root, device);
20
+ }
21
+
22
+ private processed(device: string): string {
23
+ return join(this.inbox(device), '.processed');
24
+ }
25
+
26
+ async publish(message: FleetMessage): Promise<void> {
27
+ const dir = this.inbox(message.to.device);
28
+ await mkdir(dir, { recursive: true });
29
+ const file = join(dir, `${message.msgId}.json`);
30
+ await mkdir(dirname(file), { recursive: true });
31
+ await writeFile(file, JSON.stringify(message), 'utf-8');
32
+ }
33
+
34
+ async drain(recipient: FleetEndpoint): Promise<FleetMessage[]> {
35
+ const dir = this.inbox(recipient.device);
36
+ await mkdir(dir, { recursive: true });
37
+ const names = (await readdir(dir)).filter((n) => n.endsWith('.json')).sort();
38
+ if (names.length === 0) return [];
39
+ const processed = this.processed(recipient.device);
40
+ await mkdir(processed, { recursive: true });
41
+
42
+ const messages: FleetMessage[] = [];
43
+ for (const name of names) {
44
+ const from = join(dir, name);
45
+ let parsed: FleetMessage;
46
+ try {
47
+ parsed = JSON.parse(await readFile(from, 'utf-8')) as FleetMessage;
48
+ } catch {
49
+ // 半写/损坏:移走避免反复失败(诚实跳过;至少一次语义下不静默丢失语义事件)
50
+ await rename(from, join(processed, `${name}.corrupt`));
51
+ continue;
52
+ }
53
+ await rename(from, join(processed, name));
54
+ messages.push(parsed);
55
+ }
56
+ return messages;
57
+ }
58
+
59
+ async close(): Promise<void> {
60
+ // 无长连接;无需清理。
61
+ }
62
+ }
@@ -0,0 +1,27 @@
1
+ // Fleet 传输抽象(M2 用本地 spool;M3 换 core 记忆同步,不改 runtime)。
2
+ //
3
+ // 传输层只负责把**显式状态事件**在设备间投递;语义(幂等、收敛)由 model.ts 负责。
4
+ // 投递是**至少一次**:重复投递由 `eventId` 去重(见 PROTOCOL-INVARIANTS ①)。
5
+
6
+ import type { FleetEndpoint } from '../protocol/envelope.js';
7
+ import type { TaskEvent } from '../protocol/events.js';
8
+
9
+ /** 传输消息:一次投递(可重复;`event.eventId` 为幂等键)。 */
10
+ export interface FleetMessage {
11
+ /** 传输层消息 id(每次投递唯一,与 `event.eventId` 无关) */
12
+ msgId: string;
13
+ from: FleetEndpoint;
14
+ to: FleetEndpoint;
15
+ event: TaskEvent;
16
+ }
17
+
18
+ /** 设备间传输。`drain` 拉取并消费本端收件箱(已消费的不得再次返回;崩溃时允许重投)。 */
19
+ export interface TaskTransport {
20
+ publish(message: FleetMessage): Promise<void>;
21
+ /**
22
+ * 拉取发往 `recipient` 的未消费消息(确定性顺序);拉取后标记为已消费。
23
+ * 至少一次:崩溃可能重投,故消费方必须按 `event.eventId` 幂等。
24
+ */
25
+ drain(recipient: FleetEndpoint): Promise<FleetMessage[]>;
26
+ close(): Promise<void>;
27
+ }
package/README.md DELETED
@@ -1,3 +0,0 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.