@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.
- package/dist/agentdir.d.ts +43 -0
- package/dist/agentdir.d.ts.map +1 -0
- package/dist/agentdir.js +107 -0
- package/dist/agentdir.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +1135 -0
- package/dist/cli.js.map +1 -0
- package/dist/collab/chatter.d.ts +54 -0
- package/dist/collab/chatter.d.ts.map +1 -0
- package/dist/collab/chatter.js +94 -0
- package/dist/collab/chatter.js.map +1 -0
- package/dist/collab/dag.d.ts +27 -0
- package/dist/collab/dag.d.ts.map +1 -0
- package/dist/collab/dag.js +107 -0
- package/dist/collab/dag.js.map +1 -0
- package/dist/collab/negotiation.d.ts +60 -0
- package/dist/collab/negotiation.d.ts.map +1 -0
- package/dist/collab/negotiation.js +90 -0
- package/dist/collab/negotiation.js.map +1 -0
- package/dist/collab/tree.d.ts +39 -0
- package/dist/collab/tree.d.ts.map +1 -0
- package/dist/collab/tree.js +177 -0
- package/dist/collab/tree.js.map +1 -0
- package/dist/config.d.ts +123 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +206 -0
- package/dist/config.js.map +1 -0
- package/dist/index.d.ts +33 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +40 -0
- package/dist/index.js.map +1 -0
- package/dist/join.d.ts +50 -0
- package/dist/join.d.ts.map +1 -0
- package/dist/join.js +167 -0
- package/dist/join.js.map +1 -0
- package/dist/jointoken.d.ts +144 -0
- package/dist/jointoken.d.ts.map +1 -0
- package/dist/jointoken.js +472 -0
- package/dist/jointoken.js.map +1 -0
- package/dist/model.d.ts +64 -0
- package/dist/model.d.ts.map +1 -0
- package/dist/model.js +158 -0
- package/dist/model.js.map +1 -0
- package/dist/onboard.d.ts +90 -0
- package/dist/onboard.d.ts.map +1 -0
- package/dist/onboard.js +510 -0
- package/dist/onboard.js.map +1 -0
- package/dist/protocol/envelope.d.ts +94 -0
- package/dist/protocol/envelope.d.ts.map +1 -0
- package/dist/protocol/envelope.js +169 -0
- package/dist/protocol/envelope.js.map +1 -0
- package/dist/protocol/events.d.ts +51 -0
- package/dist/protocol/events.d.ts.map +1 -0
- package/dist/protocol/events.js +87 -0
- package/dist/protocol/events.js.map +1 -0
- package/dist/qr.d.ts +15 -0
- package/dist/qr.d.ts.map +1 -0
- package/dist/qr.js +52 -0
- package/dist/qr.js.map +1 -0
- package/dist/quickstart.d.ts +167 -0
- package/dist/quickstart.d.ts.map +1 -0
- package/dist/quickstart.js +482 -0
- package/dist/quickstart.js.map +1 -0
- package/dist/quota.d.ts +41 -0
- package/dist/quota.d.ts.map +1 -0
- package/dist/quota.js +84 -0
- package/dist/quota.js.map +1 -0
- package/dist/runtime/agent.d.ts +100 -0
- package/dist/runtime/agent.d.ts.map +1 -0
- package/dist/runtime/agent.js +260 -0
- package/dist/runtime/agent.js.map +1 -0
- package/dist/runtime/executor.d.ts +42 -0
- package/dist/runtime/executor.d.ts.map +1 -0
- package/dist/runtime/executor.js +87 -0
- package/dist/runtime/executor.js.map +1 -0
- package/dist/runtime/fairness.d.ts +25 -0
- package/dist/runtime/fairness.d.ts.map +1 -0
- package/dist/runtime/fairness.js +79 -0
- package/dist/runtime/fairness.js.map +1 -0
- package/dist/runtime/node.d.ts +91 -0
- package/dist/runtime/node.d.ts.map +1 -0
- package/dist/runtime/node.js +203 -0
- package/dist/runtime/node.js.map +1 -0
- package/dist/runtime/openchamber-http.d.ts +27 -0
- package/dist/runtime/openchamber-http.d.ts.map +1 -0
- package/dist/runtime/openchamber-http.js +118 -0
- package/dist/runtime/openchamber-http.js.map +1 -0
- package/dist/runtime/openchamber.d.ts +38 -0
- package/dist/runtime/openchamber.d.ts.map +1 -0
- package/dist/runtime/openchamber.js +43 -0
- package/dist/runtime/openchamber.js.map +1 -0
- package/dist/runtime/planner.d.ts +23 -0
- package/dist/runtime/planner.d.ts.map +1 -0
- package/dist/runtime/planner.js +17 -0
- package/dist/runtime/planner.js.map +1 -0
- package/dist/runtime/worker.d.ts +96 -0
- package/dist/runtime/worker.d.ts.map +1 -0
- package/dist/runtime/worker.js +296 -0
- package/dist/runtime/worker.js.map +1 -0
- package/dist/security.d.ts +462 -0
- package/dist/security.d.ts.map +1 -0
- package/dist/security.js +1197 -0
- package/dist/security.js.map +1 -0
- package/dist/store/daemon-store.d.ts +33 -0
- package/dist/store/daemon-store.d.ts.map +1 -0
- package/dist/store/daemon-store.js +115 -0
- package/dist/store/daemon-store.js.map +1 -0
- package/dist/store/file-store.d.ts +22 -0
- package/dist/store/file-store.d.ts.map +1 -0
- package/dist/store/file-store.js +63 -0
- package/dist/store/file-store.js.map +1 -0
- package/dist/store/mebular-store.d.ts +22 -0
- package/dist/store/mebular-store.d.ts.map +1 -0
- package/dist/store/mebular-store.js +53 -0
- package/dist/store/mebular-store.js.map +1 -0
- package/dist/store/message-store.d.ts +26 -0
- package/dist/store/message-store.d.ts.map +1 -0
- package/dist/store/message-store.js +51 -0
- package/dist/store/message-store.js.map +1 -0
- package/dist/surface.d.ts +43 -0
- package/dist/surface.d.ts.map +1 -0
- package/dist/surface.js +515 -0
- package/dist/surface.js.map +1 -0
- package/dist/transport/null.d.ts +7 -0
- package/dist/transport/null.d.ts.map +1 -0
- package/dist/transport/null.js +16 -0
- package/dist/transport/null.js.map +1 -0
- package/dist/transport/spool.d.ts +12 -0
- package/dist/transport/spool.d.ts.map +1 -0
- package/dist/transport/spool.js +54 -0
- package/dist/transport/spool.js.map +1 -0
- package/dist/transport/types.d.ts +21 -0
- package/dist/transport/types.d.ts.map +1 -0
- package/dist/transport/types.js +6 -0
- package/dist/transport/types.js.map +1 -0
- package/dist/version.json +6 -0
- package/package.json +37 -4
- package/protocol/collab.example.json +23 -0
- package/protocol/daemon-home.example.json +66 -0
- package/protocol/envelope.example.json +12 -0
- package/protocol/state-machine.json +14 -0
- package/protocol/task-tree.example.json +72 -0
- package/src/agentdir.ts +138 -0
- package/src/cli.ts +1192 -0
- package/src/collab/chatter.ts +116 -0
- package/src/collab/dag.ts +124 -0
- package/src/collab/negotiation.ts +130 -0
- package/src/collab/tree.ts +184 -0
- package/src/config.ts +286 -0
- package/src/index.ts +39 -0
- package/src/join.ts +227 -0
- package/src/jointoken.ts +609 -0
- package/src/model.ts +203 -0
- package/src/onboard.ts +601 -0
- package/src/protocol/envelope.ts +228 -0
- package/src/protocol/events.ts +135 -0
- package/src/qr.ts +68 -0
- package/src/quickstart.ts +632 -0
- package/src/quota.ts +112 -0
- package/src/runtime/agent.ts +313 -0
- package/src/runtime/executor.ts +114 -0
- package/src/runtime/fairness.ts +90 -0
- package/src/runtime/node.ts +250 -0
- package/src/runtime/openchamber-http.ts +137 -0
- package/src/runtime/openchamber.ts +66 -0
- package/src/runtime/planner.ts +36 -0
- package/src/runtime/worker.ts +354 -0
- package/src/security.ts +1660 -0
- package/src/store/daemon-store.ts +144 -0
- package/src/store/file-store.ts +75 -0
- package/src/store/mebular-store.ts +66 -0
- package/src/store/message-store.ts +62 -0
- package/src/surface.ts +535 -0
- package/src/transport/null.ts +20 -0
- package/src/transport/spool.ts +62 -0
- package/src/transport/types.ts +27 -0
- package/README.md +0 -3
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
// Fleet 节点(M2):任务板 + 发起端。写入任务(created 事件)→ 投递 → 收集结果事件 → 观察收敛。
|
|
2
|
+
|
|
3
|
+
import { randomUUID } from 'node:crypto';
|
|
4
|
+
import type { FleetEndpoint, FleetTrace, TaskBudget, TaskDispatch } from '../protocol/envelope.js';
|
|
5
|
+
import type { TaskEvent } from '../protocol/events.js';
|
|
6
|
+
import { reduceTaskEvents, type TaskState } from '../model.js';
|
|
7
|
+
import type { LocalQuota, QuotaDecision } from '../quota.js';
|
|
8
|
+
import type { TaskEventStore } from '../store/file-store.js';
|
|
9
|
+
import type { MebularMessageStore } from '../store/message-store.js';
|
|
10
|
+
import type { TaskTransport } from '../transport/types.js';
|
|
11
|
+
import { dagCompletion, deriveEdges, type DagCompletion } from '../collab/dag.js';
|
|
12
|
+
import { NegotiationTracker, validateNegotiationMessage, type NegotiationMessage } from '../collab/negotiation.js';
|
|
13
|
+
import { admitCreated, admissibleTaskIds } from '../collab/tree.js';
|
|
14
|
+
|
|
15
|
+
export interface FleetNodeOptions {
|
|
16
|
+
device: string;
|
|
17
|
+
/** 发起端 Agent 名(默认 `board`) */
|
|
18
|
+
agent?: string;
|
|
19
|
+
store: TaskEventStore;
|
|
20
|
+
transport: TaskTransport;
|
|
21
|
+
quota: LocalQuota;
|
|
22
|
+
/**
|
|
23
|
+
* 1d-b:有限协商(可选)。`respond=true` 时,发起端对未终态任务的协商消息
|
|
24
|
+
* **自动接受**(确定性策略)——使 `clarify/counter → accept → 执行` 的 E2E 可自动推进。
|
|
25
|
+
*/
|
|
26
|
+
negotiation?: {
|
|
27
|
+
store: MebularMessageStore<NegotiationMessage>;
|
|
28
|
+
maxRounds: number;
|
|
29
|
+
/** 发起端策略:`accept`(默认)接受;`counter` 反提案(与 worker 交替直到超限) */
|
|
30
|
+
policy?: 'accept' | 'counter';
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface SubmitRequest {
|
|
35
|
+
intent: string;
|
|
36
|
+
to: FleetEndpoint;
|
|
37
|
+
payloadRef?: string;
|
|
38
|
+
expiresAt?: number;
|
|
39
|
+
/** 因果链(默认空;用于 DAG 派生:`causedBy` 指向父任务) */
|
|
40
|
+
trace?: FleetTrace;
|
|
41
|
+
/** 重复投递该 created 事件(同 eventId、不同 msgId)以验证幂等(默认 1) */
|
|
42
|
+
deliveries?: number;
|
|
43
|
+
/** **W1**:root 子树预算(缺省=默认上界);子任务应 ≤ 父剩余(入口不变式判定) */
|
|
44
|
+
budget?: TaskBudget;
|
|
45
|
+
/** **W1**:root 派发策略(缺省 `children-ok`) */
|
|
46
|
+
dispatch?: TaskDispatch;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface SubmitResult {
|
|
50
|
+
taskId: string | null;
|
|
51
|
+
decision: QuotaDecision;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export class FleetNode {
|
|
55
|
+
readonly endpoint: FleetEndpoint;
|
|
56
|
+
private readonly store: TaskEventStore;
|
|
57
|
+
private readonly transport: TaskTransport;
|
|
58
|
+
private readonly quota: LocalQuota;
|
|
59
|
+
private readonly negotiation: FleetNodeOptions['negotiation'] | null;
|
|
60
|
+
|
|
61
|
+
constructor(options: FleetNodeOptions) {
|
|
62
|
+
this.endpoint = { device: options.device, agent: options.agent ?? 'board' };
|
|
63
|
+
this.store = options.store;
|
|
64
|
+
this.transport = options.transport;
|
|
65
|
+
this.quota = options.quota;
|
|
66
|
+
this.negotiation = options.negotiation ?? null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* 1d-b:对未终态任务应用发起端协商策略——未接受且未超限时**接受**
|
|
71
|
+
* (确定性;`messageId` 幂等去重保证只发一次)。
|
|
72
|
+
*/
|
|
73
|
+
async applyNegotiationPolicy(): Promise<number> {
|
|
74
|
+
if (!this.negotiation) return 0;
|
|
75
|
+
const policy = this.negotiation.policy ?? 'accept';
|
|
76
|
+
const all = await this.negotiation.store.all();
|
|
77
|
+
let responded = 0;
|
|
78
|
+
for (const state of await this.states()) {
|
|
79
|
+
if (state.terminal) continue;
|
|
80
|
+
const tracker = new NegotiationTracker(state.taskId, { maxRounds: this.negotiation.maxRounds });
|
|
81
|
+
for (const message of all) {
|
|
82
|
+
if (message.taskId === state.taskId && validateNegotiationMessage(message).ok) tracker.apply(message);
|
|
83
|
+
}
|
|
84
|
+
const status = tracker.status();
|
|
85
|
+
if (status.accepted || status.rejected || status.exceeded) continue;
|
|
86
|
+
const messages = tracker.messages();
|
|
87
|
+
const last = messages[messages.length - 1];
|
|
88
|
+
// 只在对端(worker)发言后回应,避免自我循环
|
|
89
|
+
if (last === undefined || last.from.device === this.endpoint.device) continue;
|
|
90
|
+
const reply: NegotiationMessage =
|
|
91
|
+
policy === 'accept'
|
|
92
|
+
? { v: 1, messageId: `${state.taskId}#accept@${this.endpoint.device}`, taskId: state.taskId, round: status.rounds, from: this.endpoint, kind: 'accept', text: 'accepted' }
|
|
93
|
+
: { v: 1, messageId: `${state.taskId}#neg#${status.rounds + 1}@${this.endpoint.device}`, taskId: state.taskId, round: status.rounds + 1, from: this.endpoint, kind: 'counter', text: 'counter' };
|
|
94
|
+
if (await this.negotiation.store.append(reply)) responded += 1;
|
|
95
|
+
}
|
|
96
|
+
return responded;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* 1d-a:等待从 `rootTaskId` 可达的**全部节点终态**(dagCompletion)。
|
|
101
|
+
* 返回完成判定(含可达/待决节点),便于断言与汇总。
|
|
102
|
+
*/
|
|
103
|
+
async waitForDagCompletion(
|
|
104
|
+
rootTaskId: string,
|
|
105
|
+
options: { timeoutMs: number; pollMs?: number },
|
|
106
|
+
): Promise<DagCompletion> {
|
|
107
|
+
const pollMs = options.pollMs ?? 10;
|
|
108
|
+
const deadline = Date.now() + options.timeoutMs;
|
|
109
|
+
let completion: DagCompletion = dagCompletion([], () => false, rootTaskId);
|
|
110
|
+
for (;;) {
|
|
111
|
+
await this.pollOnce();
|
|
112
|
+
await this.applyNegotiationPolicy();
|
|
113
|
+
const states = await this.states();
|
|
114
|
+
const terminal = new Set(states.filter((s) => s.terminal).map((s) => s.taskId));
|
|
115
|
+
completion = dagCompletion(deriveEdges(states), (id) => terminal.has(id), rootTaskId);
|
|
116
|
+
if (completion.complete || Date.now() >= deadline) return completion;
|
|
117
|
+
await new Promise((resolve) => setTimeout(resolve, pollMs));
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** 1d-a:审查 DAG 汇总——从 root 可达且终态的节点结果按 taskId 确定序拼接。 */
|
|
122
|
+
async summarizeDag(rootTaskId: string): Promise<{ root: string; reachable: string[]; pending: string[]; summary: string }> {
|
|
123
|
+
const states = await this.states();
|
|
124
|
+
const terminal = new Set(states.filter((s) => s.terminal).map((s) => s.taskId));
|
|
125
|
+
const completion = dagCompletion(deriveEdges(states), (id) => terminal.has(id), rootTaskId);
|
|
126
|
+
const byId = new Map(states.map((s) => [s.taskId, s]));
|
|
127
|
+
const summary = completion.reachable
|
|
128
|
+
.filter((id) => terminal.has(id))
|
|
129
|
+
.map((id) => `${id}=${byId.get(id)?.resultRef ?? ''}`)
|
|
130
|
+
.join('|');
|
|
131
|
+
return { root: completion.root, reachable: completion.reachable, pending: completion.pending, summary };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
private async emit(event: TaskEvent, to: FleetEndpoint, deliveries = 1): Promise<void> {
|
|
135
|
+
await this.store.append(event);
|
|
136
|
+
for (let i = 0; i < deliveries; i++) {
|
|
137
|
+
await this.transport.publish({ msgId: randomUUID(), from: this.endpoint, to, event });
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** 提交任务:本地配额判定 → 写 `created` 事件 → 投递(可重复投递)。 */
|
|
142
|
+
async submit(request: SubmitRequest): Promise<SubmitResult> {
|
|
143
|
+
const decision = this.quota.decide(this.endpoint.device);
|
|
144
|
+
if (decision === 'rejected') return { taskId: null, decision };
|
|
145
|
+
|
|
146
|
+
const taskId = `task-${this.endpoint.device}-${randomUUID().slice(0, 8)}`;
|
|
147
|
+
const event: TaskEvent = {
|
|
148
|
+
v: 1,
|
|
149
|
+
eventId: `${taskId}#created`,
|
|
150
|
+
taskId,
|
|
151
|
+
type: 'created',
|
|
152
|
+
actor: this.endpoint,
|
|
153
|
+
at: Date.now(),
|
|
154
|
+
toStatus: 'queued',
|
|
155
|
+
trace: request.trace ?? { chain: [] },
|
|
156
|
+
to: request.to,
|
|
157
|
+
intent: request.intent,
|
|
158
|
+
};
|
|
159
|
+
if (request.payloadRef !== undefined) event.payloadRef = request.payloadRef;
|
|
160
|
+
if (request.expiresAt !== undefined) event.expiresAt = request.expiresAt;
|
|
161
|
+
if (request.budget !== undefined) event.budget = request.budget;
|
|
162
|
+
if (request.dispatch !== undefined) event.dispatch = request.dispatch;
|
|
163
|
+
await this.emit(event, request.to, request.deliveries ?? 1);
|
|
164
|
+
return { taskId, decision };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** 处理收件箱:把收到的状态事件幂等并入本地存储。返回本次新并入的事件数。 */
|
|
168
|
+
async pollOnce(): Promise<number> {
|
|
169
|
+
const messages = await this.transport.drain(this.endpoint);
|
|
170
|
+
let applied = 0;
|
|
171
|
+
let states = await this.states();
|
|
172
|
+
for (const message of messages) {
|
|
173
|
+
const event = message.event;
|
|
174
|
+
// W1 入口拒收:越预算/越链长/root-only 的 created 不入库(纯函数、全端一致)
|
|
175
|
+
if (event.type === 'created') {
|
|
176
|
+
const verdict = admitCreated(event, states, reduceTaskEvents);
|
|
177
|
+
if (!verdict.ok) continue;
|
|
178
|
+
}
|
|
179
|
+
if (await this.store.append(event)) {
|
|
180
|
+
applied += 1;
|
|
181
|
+
const reduced = reduceTaskEvents(await this.store.byTask(event.taskId));
|
|
182
|
+
if (reduced !== null) states = states.filter((s) => s.taskId !== reduced.taskId).concat(reduced);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return applied;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** 权威任务视图(已剔除违反树不变式的任务);供 DAG/汇总与验收读取。 */
|
|
189
|
+
async statesView(): Promise<TaskState[]> {
|
|
190
|
+
return this.states();
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** 当前所有任务的权威状态(按 taskId 排序)。 */
|
|
194
|
+
async states(): Promise<TaskState[]> {
|
|
195
|
+
const events = await this.store.all();
|
|
196
|
+
const byTask = new Map<string, TaskEvent[]>();
|
|
197
|
+
for (const event of events) {
|
|
198
|
+
const list = byTask.get(event.taskId);
|
|
199
|
+
if (list) list.push(event);
|
|
200
|
+
else byTask.set(event.taskId, [event]);
|
|
201
|
+
}
|
|
202
|
+
const out: TaskState[] = [];
|
|
203
|
+
for (const taskId of [...byTask.keys()].sort()) {
|
|
204
|
+
const state = reduceTaskEvents(byTask.get(taskId)!);
|
|
205
|
+
if (state) out.push(state);
|
|
206
|
+
}
|
|
207
|
+
// W1:权威视图剔除违反树不变式的任务(经记忆同步到达的无效 created 亦生效)
|
|
208
|
+
const admissible = admissibleTaskIds(out);
|
|
209
|
+
return out.filter((s) => admissible.has(s.taskId));
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
async stateOf(taskId: string): Promise<TaskState | null> {
|
|
213
|
+
return reduceTaskEvents(await this.store.byTask(taskId));
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** 轮询直到给定任务全部终态(或超时)。返回是否全部终态。 */
|
|
217
|
+
async waitForTerminal(
|
|
218
|
+
taskIds: readonly string[],
|
|
219
|
+
options: { timeoutMs: number; pollMs?: number },
|
|
220
|
+
): Promise<boolean> {
|
|
221
|
+
const pollMs = options.pollMs ?? 10;
|
|
222
|
+
const deadline = Date.now() + options.timeoutMs;
|
|
223
|
+
const pending = new Set(taskIds);
|
|
224
|
+
while (Date.now() < deadline) {
|
|
225
|
+
await this.pollOnce();
|
|
226
|
+
await this.applyNegotiationPolicy();
|
|
227
|
+
const states = await this.states();
|
|
228
|
+
for (const state of states) if (state.terminal) pending.delete(state.taskId);
|
|
229
|
+
if (pending.size === 0) return true;
|
|
230
|
+
await new Promise((resolve) => setTimeout(resolve, pollMs));
|
|
231
|
+
}
|
|
232
|
+
return pending.size === 0;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** 清空收件箱(把当前待处理消息并入存储)。便于测试可控推进。 */
|
|
236
|
+
async settle(): Promise<number> {
|
|
237
|
+
let total = 0;
|
|
238
|
+
let n = await this.pollOnce();
|
|
239
|
+
while (n > 0) {
|
|
240
|
+
total += n;
|
|
241
|
+
n = await this.pollOnce();
|
|
242
|
+
}
|
|
243
|
+
return total;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
async close(): Promise<void> {
|
|
247
|
+
await this.transport.close();
|
|
248
|
+
await this.store.close();
|
|
249
|
+
}
|
|
250
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
// OpenChamber 适配器(M4):**中立 HTTP 客户端 seam**。
|
|
2
|
+
//
|
|
3
|
+
// 本文件对 provider **毫不知情**:只接受「一个 HTTPS/HTTP 端点 + 一个 bearer-ish token(内联或从文件读)
|
|
4
|
+
// + 超时 + 可选 agent/model」,POST 一个 JSON 请求并读取 `{ ok, result:{ text, sessionId? }, error? }`。
|
|
5
|
+
// 当前 provider #1 是 oc-hermes-bridge daemon 的 `POST /agent/run-once`(由调用方/脚本提供 endpoint 与
|
|
6
|
+
// token 路径);将来换成独立 provider 或 OpenChamber 官方 API,**不改本文件、也不改 fleet 代码**。
|
|
7
|
+
//
|
|
8
|
+
// 安全:只发本机端点、token 不硬编码、不打印、不落盘、不出现在错误信息里;超时/连接失败 → 归一错误。
|
|
9
|
+
|
|
10
|
+
import { readFileSync } from 'node:fs';
|
|
11
|
+
import http from 'node:http';
|
|
12
|
+
|
|
13
|
+
import type { OpenChamberPromptInput, OpenChamberSessionSeam } from './openchamber.js';
|
|
14
|
+
|
|
15
|
+
export interface HttpOpenChamberSeamOptions {
|
|
16
|
+
/** 完整端点 URL(provider 决定路径);缺省读 env `MEBULAR_FLEET_OPENCHAMBER_ENDPOINT` */
|
|
17
|
+
endpoint?: string;
|
|
18
|
+
/** 内联 token;缺省 `MEBULAR_FLEET_OPENCHAMBER_TOKEN` */
|
|
19
|
+
token?: string;
|
|
20
|
+
/** 从文件读 token;缺省 `MEBULAR_FLEET_OPENCHAMBER_TOKEN_FILE` */
|
|
21
|
+
tokenFile?: string;
|
|
22
|
+
/** 文件为 JSON 时取该点分路径;缺省 `MEBULAR_FLEET_OPENCHAMBER_TOKEN_JSON_PATH` 或 `token` */
|
|
23
|
+
tokenJsonPath?: string;
|
|
24
|
+
/** 鉴权头名(provider 决定,默认 `X-Bridge-Token`);缺省 `MEBULAR_FLEET_OPENCHAMBER_AUTH_HEADER` */
|
|
25
|
+
authHeader?: string;
|
|
26
|
+
/** 等待上限(默认 600000ms,且不超过 600s) */
|
|
27
|
+
timeoutMs?: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface BridgeReply {
|
|
31
|
+
ok: boolean;
|
|
32
|
+
result?: { text?: string; sessionId?: string; ms?: number };
|
|
33
|
+
error?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function env(name: string): string | undefined {
|
|
37
|
+
const value = process.env[name];
|
|
38
|
+
return value === undefined || value === '' ? undefined : value;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** 读取 token:文件是 JSON 则按 `jsonPath` 取值,否则取整文件文本。 */
|
|
42
|
+
function readTokenFromFile(file: string, jsonPath: string): string | null {
|
|
43
|
+
try {
|
|
44
|
+
const raw = readFileSync(file, 'utf-8').trim();
|
|
45
|
+
if (raw.startsWith('{')) {
|
|
46
|
+
let node: unknown = JSON.parse(raw);
|
|
47
|
+
for (const key of jsonPath.split('.')) {
|
|
48
|
+
if (typeof node !== 'object' || node === null) return null;
|
|
49
|
+
node = (node as Record<string, unknown>)[key];
|
|
50
|
+
}
|
|
51
|
+
return typeof node === 'string' && node.length > 0 ? node : null;
|
|
52
|
+
}
|
|
53
|
+
return raw.length > 0 ? raw : null;
|
|
54
|
+
} catch {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** 本机 HTTP POST JSON;连接失败/超时/非 JSON → null 或 `{ok:false}`;错误不夹带 token。 */
|
|
60
|
+
function postJson(endpoint: string, token: string, authHeader: string, body: string, timeoutMs: number): Promise<BridgeReply | null> {
|
|
61
|
+
return new Promise((resolve) => {
|
|
62
|
+
let url: URL;
|
|
63
|
+
try {
|
|
64
|
+
url = new URL(endpoint);
|
|
65
|
+
} catch {
|
|
66
|
+
resolve(null);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
const req = http.request(
|
|
70
|
+
{
|
|
71
|
+
host: url.hostname,
|
|
72
|
+
port: url.port,
|
|
73
|
+
path: url.pathname + url.search,
|
|
74
|
+
method: 'POST',
|
|
75
|
+
headers: {
|
|
76
|
+
'content-type': 'application/json',
|
|
77
|
+
'content-length': Buffer.byteLength(body),
|
|
78
|
+
[authHeader]: token,
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
(res) => {
|
|
82
|
+
let data = '';
|
|
83
|
+
res.on('data', (chunk) => (data += chunk));
|
|
84
|
+
res.on('end', () => {
|
|
85
|
+
try {
|
|
86
|
+
resolve(JSON.parse(data) as BridgeReply);
|
|
87
|
+
} catch {
|
|
88
|
+
resolve({ ok: false, error: `provider returned non-JSON (status ${res.statusCode ?? '?'})` });
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
},
|
|
92
|
+
);
|
|
93
|
+
req.on('error', () => resolve(null)); // 不回传底层细节(避免泄露端点/凭据)
|
|
94
|
+
req.setTimeout(Math.max(1, timeoutMs), () => {
|
|
95
|
+
req.destroy();
|
|
96
|
+
resolve(null);
|
|
97
|
+
});
|
|
98
|
+
req.write(body);
|
|
99
|
+
req.end();
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** 中立 HTTP seam(provider 只决定 endpoint/token/字段)。 */
|
|
104
|
+
export class HttpOpenChamberSeam implements OpenChamberSessionSeam {
|
|
105
|
+
constructor(private readonly options: HttpOpenChamberSeamOptions = {}) {}
|
|
106
|
+
|
|
107
|
+
private resolveToken(): string | null {
|
|
108
|
+
const inline = this.options.token ?? env('MEBULAR_FLEET_OPENCHAMBER_TOKEN');
|
|
109
|
+
if (inline !== undefined) return inline;
|
|
110
|
+
const file = this.options.tokenFile ?? env('MEBULAR_FLEET_OPENCHAMBER_TOKEN_FILE');
|
|
111
|
+
if (file === undefined) return null;
|
|
112
|
+
const jsonPath = this.options.tokenJsonPath ?? env('MEBULAR_FLEET_OPENCHAMBER_TOKEN_JSON_PATH') ?? 'token';
|
|
113
|
+
return readTokenFromFile(file, jsonPath);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async prompt(input: OpenChamberPromptInput): Promise<{ text: string; sessionId?: string; error?: string }> {
|
|
117
|
+
const endpoint = this.options.endpoint ?? env('MEBULAR_FLEET_OPENCHAMBER_ENDPOINT');
|
|
118
|
+
if (endpoint === undefined) return { text: '', error: 'openchamber endpoint not configured' };
|
|
119
|
+
const token = this.resolveToken();
|
|
120
|
+
if (token === null) return { text: '', error: 'openchamber token unavailable' };
|
|
121
|
+
|
|
122
|
+
const timeoutMs = Math.max(1, Math.min(input.timeoutMs ?? this.options.timeoutMs ?? 600_000, 600_000));
|
|
123
|
+
const payload: Record<string, unknown> = { prompt: input.prompt, timeoutSec: Math.ceil(timeoutMs / 1000) };
|
|
124
|
+
if (input.agent !== undefined) payload.agent = input.agent;
|
|
125
|
+
if (input.model !== undefined) payload.model = input.model;
|
|
126
|
+
|
|
127
|
+
const authHeader = this.options.authHeader ?? env('MEBULAR_FLEET_OPENCHAMBER_AUTH_HEADER') ?? 'X-Bridge-Token';
|
|
128
|
+
const reply = await postJson(endpoint, token, authHeader, JSON.stringify(payload), timeoutMs);
|
|
129
|
+
if (reply === null) return { text: '', error: 'openchamber provider unavailable or timed out' };
|
|
130
|
+
if (reply.ok !== true) return { text: '', error: reply.error ?? 'openchamber provider error' };
|
|
131
|
+
const result = reply.result ?? {};
|
|
132
|
+
return {
|
|
133
|
+
text: typeof result.text === 'string' ? result.text : '',
|
|
134
|
+
...(typeof result.sessionId === 'string' ? { sessionId: result.sessionId } : {}),
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// OpenChamber Agent 适配器(M4):**中立接口 + 执行器**。
|
|
2
|
+
//
|
|
3
|
+
// fleet 对 provider 毫不知情:`OpenChamberAgent` 只依赖注入的 `OpenChamberSessionSeam`(prompt →
|
|
4
|
+
// {text, sessionId?, error?}),选项仅 `timeoutMs/maxOutputBytes/cwd/model/agent`。具体 provider
|
|
5
|
+
// 由调用方注入(字段与错误码见 `packages/fleet/OPENCHAMBER-SEAM.md`)。
|
|
6
|
+
// 当前 provider #1 是 oc-hermes-bridge daemon 的 `POST /agent/run-once`(见 `openchamber-http.ts`
|
|
7
|
+
// 的中立 HTTP seam);将来换成独立 provider 或 OpenChamber 官方 API,**不改本文件**。
|
|
8
|
+
|
|
9
|
+
import type { TaskState } from '../model.js';
|
|
10
|
+
import type { ExecutionOutcome, TaskExecutor } from './executor.js';
|
|
11
|
+
import { truncateOutput } from './agent.js';
|
|
12
|
+
|
|
13
|
+
/** 一次会话提示的输入。 */
|
|
14
|
+
export interface OpenChamberPromptInput {
|
|
15
|
+
prompt: string;
|
|
16
|
+
cwd?: string;
|
|
17
|
+
model?: string;
|
|
18
|
+
agent?: string;
|
|
19
|
+
/** 期望的最长等待(seam 实现负责超时并在超时后抛错/返回失败) */
|
|
20
|
+
timeoutMs?: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** OpenChamber 会话 seam:把「运行一次提示」抽象出来,交由 OpenChamber 侧实现。 */
|
|
24
|
+
export interface OpenChamberSessionSeam {
|
|
25
|
+
/**
|
|
26
|
+
* 运行一次 OpenChamber 会话提示,返回最终文本与(可选)会话 id。
|
|
27
|
+
* 超时/失败应以异常或 `{ error }` 表达;实现方不得在此打印/落盘任何凭据。
|
|
28
|
+
*/
|
|
29
|
+
prompt(input: OpenChamberPromptInput): Promise<{ text: string; sessionId?: string; error?: string }>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface OpenChamberAgentOptions {
|
|
33
|
+
timeoutMs?: number;
|
|
34
|
+
maxOutputBytes?: number;
|
|
35
|
+
cwd?: string;
|
|
36
|
+
model?: string;
|
|
37
|
+
agent?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** 基于注入 seam 的 OpenChamber 执行器(默认不接任何真实通道)。 */
|
|
41
|
+
export class OpenChamberAgent implements TaskExecutor {
|
|
42
|
+
constructor(
|
|
43
|
+
private readonly seam: OpenChamberSessionSeam,
|
|
44
|
+
private readonly options: OpenChamberAgentOptions = {},
|
|
45
|
+
) {}
|
|
46
|
+
|
|
47
|
+
async execute(task: TaskState): Promise<ExecutionOutcome> {
|
|
48
|
+
const input: OpenChamberPromptInput = { prompt: task.intent };
|
|
49
|
+
if (this.options.cwd !== undefined) input.cwd = this.options.cwd;
|
|
50
|
+
if (this.options.model !== undefined) input.model = this.options.model;
|
|
51
|
+
if (this.options.agent !== undefined) input.agent = this.options.agent;
|
|
52
|
+
if (this.options.timeoutMs !== undefined) input.timeoutMs = this.options.timeoutMs;
|
|
53
|
+
try {
|
|
54
|
+
const result = await this.seam.prompt(input);
|
|
55
|
+
if (result.error !== undefined) return { ok: false, reason: `OPENCHAMBER_ERROR: ${result.error}` };
|
|
56
|
+
const truncated = truncateOutput(result.text.trim(), this.options.maxOutputBytes ?? 65_536);
|
|
57
|
+
return {
|
|
58
|
+
ok: true,
|
|
59
|
+
...(result.sessionId !== undefined ? { reason: `session:${result.sessionId}` } : {}),
|
|
60
|
+
resultRef: truncated.text,
|
|
61
|
+
};
|
|
62
|
+
} catch (error) {
|
|
63
|
+
return { ok: false, reason: `OPENCHAMBER_ERROR: ${(error as Error).message}` };
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// 任务派生计划(1d-a 审查 DAG):worker 执行任务后可按**确定性计划**派生**子任务**。
|
|
2
|
+
//
|
|
3
|
+
// 计划是纯数据/纯函数,便于夹具驱动与确定性 E2E;子任务以既有 `created` 事件承载,
|
|
4
|
+
// `trace.causedBy` 指向父任务、`chain` 为祖先链(禁环由创建守卫保证)。
|
|
5
|
+
|
|
6
|
+
import type { FleetEndpoint } from '../protocol/envelope.js';
|
|
7
|
+
import type { TaskState } from '../model.js';
|
|
8
|
+
|
|
9
|
+
/** 计划中的一个子任务。 */
|
|
10
|
+
export interface PlannedChild {
|
|
11
|
+
intent: string;
|
|
12
|
+
/** 目标端(缺省 = 父任务的 to) */
|
|
13
|
+
to?: FleetEndpoint;
|
|
14
|
+
/** 显式 taskId(缺省 = `<parent>~c<index>`,确定);**负例**可用它构造环以触发守卫 */
|
|
15
|
+
taskId?: string;
|
|
16
|
+
payloadRef?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** 计划器:由父任务与执行结果派生直接子任务。 */
|
|
20
|
+
export interface TaskPlanner {
|
|
21
|
+
plan(state: TaskState, outcome: { ok: boolean; resultRef?: string }): PlannedChild[];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** 确定性子任务 id(无显式 taskId 时使用)。 */
|
|
25
|
+
export function childTaskId(parent: string, index: number): string {
|
|
26
|
+
return `${parent}~c${index}`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** 由 `intent → 子任务[]` 映射构造计划器(缺省 = 无子任务)。 */
|
|
30
|
+
export function mapPlanner(plan: Readonly<Record<string, PlannedChild[]>>): TaskPlanner {
|
|
31
|
+
return {
|
|
32
|
+
plan(state) {
|
|
33
|
+
return (plan[state.intent] ?? []).map((child) => ({ ...child }));
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
}
|