@agent-phonon/protocol 0.3.0 → 0.6.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.
@@ -1,6 +1,17 @@
1
1
  import { z } from "zod";
2
2
  import { AgentId, ProjectId, SessionId, Timestamp } from "./common.js";
3
3
 
4
+ /**
5
+ * L3 Orchestration Protocol (workflow.*)
6
+ *
7
+ * Locked design (2026-06-23 v0.4 + v0.5 review):
8
+ * - L3 编排是 L1 session 的 wrapper,不重新发明 session 概念。
9
+ * - Node 创建出来的 L1 session 的所有 stream.event 自动带 workflowId / nodeId / role
10
+ * 字段(见 stream.ts),服务端按这些字段筛选/区分。
11
+ * workflow.event **不重复承载** session 流,只负责工作流级元事件。
12
+ * - phonon 只提供通用底层能力。category / wisdom 这类业务由上层 server 自行实现。
13
+ */
14
+
4
15
  const ModelId = z.string().min(1);
5
16
  const TurnId = z.string().min(1);
6
17
  const ClientRequestId = z.string().min(1);
@@ -8,12 +19,25 @@ const ClientRequestId = z.string().min(1);
8
19
  export const WorkflowId = z.string().min(1);
9
20
  export const WorkflowNodeId = z.string().min(1);
10
21
  export const WorkflowEdgeId = z.string().min(1);
22
+
23
+ /**
24
+ * Role 字段说明(review v0.5):
25
+ * role 是这个 agent 在当前 workflow 中扮演的角色,由调用方自由定义。
26
+ * phonon 只做透传 + 在 stream.event / workflow.event 里回显,不解析其语义。
27
+ * 常见示例:"executor" / "worker" / "reviewer" / "coder" / "planner" / "chairman" / "participant"
28
+ * 上层服务可以把 role 映射到 category / system prompt / 模型选择,phonon 不感知。
29
+ */
11
30
  export const WorkflowRoleId = z.string().min(1);
12
31
 
32
+ // ---------------------------------------------------------------------------
33
+ // Node / Edge / Plan
34
+ // ---------------------------------------------------------------------------
35
+
13
36
  export const WorkflowNode = z.object({
14
37
  nodeId: WorkflowNodeId,
15
38
  agent: AgentId,
16
39
  model: ModelId,
40
+ /** 该节点在 workflow 中扮演的角色(由调用方定义,phonon 仅回显)。 */
17
41
  role: WorkflowRoleId.optional(),
18
42
  input: z.string().optional(),
19
43
  systemPrompt: z.string().optional(),
@@ -21,6 +45,32 @@ export const WorkflowNode = z.object({
21
45
  sessionId: SessionId.optional(),
22
46
  agentConfig: z.record(z.unknown()).optional(),
23
47
  metadata: z.record(z.unknown()).optional(),
48
+
49
+ // ===== 执行环境(v0.6 改造。62026-06-23。不兼容旧 worktreeId 句柄语义)=====
50
+ /**
51
+ * 此节点跑在哪个 project。不传默认继承 workflow 级 project。
52
+ * project + workflow 级都没传,phonon 拒接。
53
+ */
54
+ project: ProjectId.optional(),
55
+ /**
56
+ * 隔离 key。调用方自定义的任意字符串。
57
+ * - 不传 → 该节点跑在 project 主目录
58
+ * - 传了 → phonon 按 (projectId, worktreeId) 复合键维护隔离 worktree:
59
+ * • 同一 workflow 内首次看到 → 创建 worktree(branch 名 phonon 自动生成)
60
+ * • 同一 workflow 内后续节点用同一 key → 复用同一 worktree
61
+ * • 不跨 workflow 复用(下次 workflow.run 同名 key 是新 worktree)
62
+ * - workflow 终态时 phonon 尝试自动清理:git status 干净则 worktree remove;dirty 则保留
63
+ * 且发 workflow.status payload warn。创出的 branch 一律不删。
64
+ */
65
+ worktreeId: z.string().optional(),
66
+ /**
67
+ * Git branch。意义联动 worktreeId:
68
+ * - 不传 worktreeId 传 branch → 在 project 主目录先 `git checkout <branch>` 再跑(对主目录有副作用,法定调用方负责)
69
+ * - 传 worktreeId 首次传 branch → 作为新 worktree 的 base branch(从它检出)
70
+ * - 传 worktreeId 未传 branch 首次 → 从 project 当前 branch 检出
71
+ * - worktreeId 复用时 branch 被忽略(worktree 已存在)
72
+ */
73
+ branch: z.string().optional(),
24
74
  });
25
75
  export type WorkflowNode = z.infer<typeof WorkflowNode>;
26
76
 
@@ -49,35 +99,155 @@ export const WorkflowCommunicationGraph = z.object({
49
99
  });
50
100
  export type WorkflowCommunicationGraph = z.infer<typeof WorkflowCommunicationGraph>;
51
101
 
102
+ export const WorkflowExecutorNode = WorkflowNode.extend({
103
+ role: z.literal("executor").default("executor"),
104
+ });
105
+ export type WorkflowExecutorNode = z.infer<typeof WorkflowExecutorNode>;
106
+
52
107
  export const WorkflowGraphPlan = z.object({
53
108
  mode: z.literal("graph"),
54
- executor: z.object({
55
- nodeId: WorkflowNodeId,
56
- agent: AgentId,
57
- model: ModelId,
58
- role: z.literal("executor").default("executor"),
59
- systemPrompt: z.string().optional(),
60
- agentConfig: z.record(z.unknown()).optional(),
61
- }),
109
+ executor: WorkflowExecutorNode,
62
110
  workers: z.array(WorkflowNode).min(1),
63
111
  communicationGraph: WorkflowCommunicationGraph,
64
112
  });
65
113
  export type WorkflowGraphPlan = z.infer<typeof WorkflowGraphPlan>;
66
114
 
67
- export const WorkflowPlan = z.discriminatedUnion("mode", [WorkflowDagPlan, WorkflowGraphPlan]);
115
+ // ---------------------------------------------------------------------------
116
+ // Discussion plan (review v0.5 借鉴 4,对应 Foreman _run_discuss_rounds)
117
+ //
118
+ // N 个 agent 平等参与多轮讨论,chairman 控场决定何时终止。
119
+ // 与 graph 模式根本区别:没有路由 + 没有主从;所有 participant 每轮并行发言。
120
+ // ---------------------------------------------------------------------------
121
+
122
+ export const WorkflowDiscussionPlan = z.object({
123
+ mode: z.literal("discussion"),
124
+ /** 主题(initial input) — 所有 participant 第一轮收到这个。 */
125
+ topic: z.string(),
126
+ /** 参与者列表(无主从)。 */
127
+ participants: z.array(WorkflowNode).min(2),
128
+ /** Chairman 必须是 participants 之一的 nodeId,每轮所有 participant 发完它再 review + 决定续/停。 */
129
+ chairman: WorkflowNodeId,
130
+ termination: z
131
+ .object({
132
+ /** chairman 输出含该信号则终止。 */
133
+ chairmanSignal: z.string().default("[DISCUSS_END]"),
134
+ /** 硬上限。 */
135
+ maxRounds: z.number().int().positive().default(10),
136
+ /** 自然达成共识的检测字符串(任一 participant 输出含此则终止,可选)。 */
137
+ consensusSignal: z.string().optional(),
138
+ })
139
+ .default({}),
140
+ });
141
+ export type WorkflowDiscussionPlan = z.infer<typeof WorkflowDiscussionPlan>;
142
+
143
+ export const WorkflowPlan = z.discriminatedUnion("mode", [
144
+ WorkflowDagPlan,
145
+ WorkflowGraphPlan,
146
+ WorkflowDiscussionPlan,
147
+ ]);
68
148
  export type WorkflowPlan = z.infer<typeof WorkflowPlan>;
69
149
 
70
- export const WorkflowStatus = z.enum(["queued", "running", "completed", "failed", "cancelled", "timeout"]);
150
+ // ---------------------------------------------------------------------------
151
+ // Status enums
152
+ // ---------------------------------------------------------------------------
153
+
154
+ export const WorkflowStatus = z.enum([
155
+ "queued",
156
+ "running",
157
+ "completed",
158
+ "failed",
159
+ "cancelled",
160
+ "timeout",
161
+ ]);
71
162
  export type WorkflowStatus = z.infer<typeof WorkflowStatus>;
72
163
 
73
- export const WorkflowNodeStatus = z.enum(["pending", "ready", "running", "completed", "failed", "skipped", "cancelled"]);
164
+ export const WorkflowNodeStatus = z.enum([
165
+ "pending",
166
+ "ready",
167
+ "running",
168
+ "completed",
169
+ "failed",
170
+ "skipped",
171
+ "cancelled",
172
+ ]);
74
173
  export type WorkflowNodeStatus = z.infer<typeof WorkflowNodeStatus>;
75
174
 
175
+ // ---------------------------------------------------------------------------
176
+ // Execution policy
177
+ // ---------------------------------------------------------------------------
178
+
179
+ export const WorkflowPolicy = z
180
+ .object({
181
+ timeoutSeconds: z.number().int().positive().optional(),
182
+ perNodeTimeoutSeconds: z.number().int().positive().optional(),
183
+ onNodeFailure: z.enum(["fail_workflow", "skip_dependents", "continue"]).default("fail_workflow"),
184
+ maxParallel: z.number().int().positive().optional(),
185
+ })
186
+ .partial();
187
+ export type WorkflowPolicy = z.infer<typeof WorkflowPolicy>;
188
+
189
+ // ---------------------------------------------------------------------------
190
+ // Shared context (review v0.5 借鉴 3:通用共享上下文,不是 SSOT)
191
+ //
192
+ // phonon 在每个 node create 时把 sharedContext 注入到 systemPrompt(追加形式)。
193
+ // 文件路径会被 phonon 解析为 workspace 内绝对路径(受 policy 沙箱保护)。
194
+ // 文件不存在则跳过该文件,不阻塞 workflow。
195
+ // ---------------------------------------------------------------------------
196
+
197
+ export const WorkflowSharedContext = z.object({
198
+ /** 直接文本:phonon 原样追加到每个 node 的 systemPrompt。 */
199
+ text: z.string().optional(),
200
+ /** workspace 相对路径列表(受 policy 沙箱);phonon 读取后拼成 markdown 段追加到 systemPrompt。 */
201
+ files: z.array(z.string()).optional(),
202
+ /** 注入位置:prepend 在最前,append 在最后,默认 append。 */
203
+ placement: z.enum(["prepend", "append"]).default("append"),
204
+ });
205
+ export type WorkflowSharedContext = z.infer<typeof WorkflowSharedContext>;
206
+
207
+ // ---------------------------------------------------------------------------
208
+ // Resume (review v0.5 借鉴 2:checkpoint + 断点续跑)
209
+ // ---------------------------------------------------------------------------
210
+
211
+ export const WorkflowResumeFrom = z.object({
212
+ /** 要恢复的 workflowId(必须先前 run 过,并有 checkpoint)。 */
213
+ workflowId: WorkflowId,
214
+ /**
215
+ * 起点策略:
216
+ * - "last_success_dependents" : 从最后成功节点的下游开始(DAG 默认)
217
+ * - "failed_node" : 重跑失败的那个 node(其他成功 node 不重做)
218
+ * - "node:<nodeId>" : 显式指定从某 node 开始
219
+ */
220
+ strategy: z.union([
221
+ z.literal("last_success_dependents"),
222
+ z.literal("failed_node"),
223
+ z.string().regex(/^node:/),
224
+ ]).default("failed_node"),
225
+ /** 可选:重置某些 node 的状态(让它们重跑),覆盖默认策略。 */
226
+ rerunNodes: z.array(WorkflowNodeId).optional(),
227
+ });
228
+ export type WorkflowResumeFrom = z.infer<typeof WorkflowResumeFrom>;
229
+
230
+ // ---------------------------------------------------------------------------
231
+ // workflow.run
232
+ // ---------------------------------------------------------------------------
233
+
76
234
  export const WorkflowRunParams = z.object({
77
- project: ProjectId,
235
+ /**
236
+ * 默认 project。可以省略,但如果某个 node 也没传 project,phonon 会拒接。
237
+ * v0.6 起改为可选,支持每个 node 跳不同 project 的跨项目编排场景。
238
+ */
239
+ project: ProjectId.optional(),
240
+ /** 默认隔离 key;node 未传 worktreeId 时继承该值。含义及复用规则同 WorkflowNode.worktreeId。 */
78
241
  worktreeId: z.string().optional(),
242
+ /** 默认 branch;node 未传 branch 时继承。含义同 WorkflowNode.branch。 */
243
+ branch: z.string().optional(),
79
244
  plan: WorkflowPlan,
80
245
  input: z.string().optional(),
246
+ policy: WorkflowPolicy.optional(),
247
+ /** 共享上下文(v0.5 新增)。 */
248
+ sharedContext: WorkflowSharedContext.optional(),
249
+ /** 断点续跑(v0.5 新增)。传入则跳过 plan 重新规划,从 checkpoint 恢复。 */
250
+ resumeFrom: WorkflowResumeFrom.optional(),
81
251
  clientRequestId: ClientRequestId.optional(),
82
252
  metadata: z.record(z.unknown()).optional(),
83
253
  });
@@ -87,11 +257,26 @@ export const WorkflowRunResult = z.object({
87
257
  workflowId: WorkflowId,
88
258
  status: WorkflowStatus,
89
259
  createdAt: Timestamp,
260
+ /** 是否走的 resume 路径。 */
261
+ resumed: z.boolean().default(false),
90
262
  });
91
263
  export type WorkflowRunResult = z.infer<typeof WorkflowRunResult>;
92
264
 
93
- export const WorkflowStatusParams = z.object({ workflowId: WorkflowId });
94
- export type WorkflowStatusParams = z.infer<typeof WorkflowStatusParams>;
265
+ // ---------------------------------------------------------------------------
266
+ // Node runtime state
267
+ // ---------------------------------------------------------------------------
268
+
269
+ export const WorkflowNodeResult = z.object({
270
+ text: z.string().optional(),
271
+ status: z.enum(["completed", "interrupted", "aborted", "failed", "timeout"]),
272
+ usage: z
273
+ .object({
274
+ inputTokens: z.number().int().nonnegative().optional(),
275
+ outputTokens: z.number().int().nonnegative().optional(),
276
+ })
277
+ .optional(),
278
+ });
279
+ export type WorkflowNodeResult = z.infer<typeof WorkflowNodeResult>;
95
280
 
96
281
  export const WorkflowNodeRuntime = z.object({
97
282
  nodeId: WorkflowNodeId,
@@ -104,19 +289,33 @@ export const WorkflowNodeRuntime = z.object({
104
289
  startedAt: Timestamp.optional(),
105
290
  completedAt: Timestamp.optional(),
106
291
  error: z.string().optional(),
292
+ result: WorkflowNodeResult.optional(),
293
+ /** 该节点完成的轮次计数(discussion 用;DAG/Graph 一次执行 = 1)。 */
294
+ iterations: z.number().int().nonnegative().optional(),
107
295
  });
108
296
  export type WorkflowNodeRuntime = z.infer<typeof WorkflowNodeRuntime>;
109
297
 
298
+ // ---------------------------------------------------------------------------
299
+ // workflow.status / list / cancel
300
+ // ---------------------------------------------------------------------------
301
+
302
+ export const WorkflowStatusParams = z.object({ workflowId: WorkflowId });
303
+ export type WorkflowStatusParams = z.infer<typeof WorkflowStatusParams>;
304
+
110
305
  export const WorkflowStatusResult = z.object({
111
306
  workflowId: WorkflowId,
112
307
  status: WorkflowStatus,
113
- project: ProjectId,
114
- mode: z.enum(["dag", "graph"]),
308
+ /** workflow 默认 project;v0.6 起可选。 */
309
+ project: ProjectId.optional(),
310
+ mode: z.enum(["dag", "graph", "discussion"]),
115
311
  nodes: z.array(WorkflowNodeRuntime),
116
312
  createdAt: Timestamp,
117
313
  updatedAt: Timestamp,
118
314
  completedAt: Timestamp.optional(),
119
315
  error: z.string().optional(),
316
+ finalText: z.string().optional(),
317
+ /** 是否可 resume:phonon 持有该 workflow 的 checkpoint。 */
318
+ resumable: z.boolean().default(false),
120
319
  });
121
320
  export type WorkflowStatusResult = z.infer<typeof WorkflowStatusResult>;
122
321
 
@@ -127,17 +326,37 @@ export type WorkflowCancelResult = z.infer<typeof WorkflowCancelResult>;
127
326
 
128
327
  export const WorkflowListParams = z.object({
129
328
  status: WorkflowStatus.optional(),
329
+ projectId: ProjectId.optional(),
330
+ since: Timestamp.optional(),
331
+ until: Timestamp.optional(),
130
332
  limit: z.number().int().positive().max(100).default(50).optional(),
131
333
  cursor: z.string().optional(),
132
334
  });
133
335
  export type WorkflowListParams = z.infer<typeof WorkflowListParams>;
134
- export const WorkflowListResult = z.object({ workflows: z.array(WorkflowStatusResult), nextCursor: z.string().optional() });
336
+ export const WorkflowListResult = z.object({
337
+ workflows: z.array(WorkflowStatusResult),
338
+ nextCursor: z.string().optional(),
339
+ });
135
340
  export type WorkflowListResult = z.infer<typeof WorkflowListResult>;
136
341
 
342
+ // ---------------------------------------------------------------------------
343
+ // workflow.event
344
+ // v0.5 新增事件类型:round.started / round.completed / discussion.terminated
345
+ // (为 Discussion 模式准备;DAG/Graph 不发这些)
346
+ // ---------------------------------------------------------------------------
347
+
137
348
  export const WorkflowEvent = z.object({
138
349
  workflowId: WorkflowId,
139
350
  seq: z.number().int().nonnegative(),
140
- type: z.enum(["workflow.status", "node.status", "node.stream", "edge.route", "executor.decision"]),
351
+ type: z.enum([
352
+ "workflow.status",
353
+ "node.status",
354
+ "edge.route",
355
+ "executor.decision",
356
+ "round.started", // discussion / graph iteration 轮次开始
357
+ "round.completed", // 一轮所有参与者发完
358
+ "discussion.terminated", // discussion 终止(含理由)
359
+ ]),
141
360
  nodeId: WorkflowNodeId.optional(),
142
361
  sessionId: SessionId.optional(),
143
362
  turnId: TurnId.optional(),
@@ -145,7 +364,76 @@ export const WorkflowEvent = z.object({
145
364
  model: ModelId.optional(),
146
365
  role: WorkflowRoleId.optional(),
147
366
  status: z.union([WorkflowStatus, WorkflowNodeStatus]).optional(),
367
+ result: WorkflowNodeResult.optional(),
368
+ /** 自由载荷。常见字段:iteration, from, to, reason, terminationReason 等。 */
148
369
  payload: z.record(z.unknown()).optional(),
149
370
  timestamp: Timestamp,
150
371
  });
151
372
  export type WorkflowEvent = z.infer<typeof WorkflowEvent>;
373
+
374
+ // ---------------------------------------------------------------------------
375
+ // workflow.ack
376
+ // ---------------------------------------------------------------------------
377
+
378
+ export const WorkflowAckParams = z.object({
379
+ workflowId: WorkflowId,
380
+ lastSeq: z.number().int().nonnegative(),
381
+ });
382
+ export type WorkflowAckParams = z.infer<typeof WorkflowAckParams>;
383
+
384
+ // ---------------------------------------------------------------------------
385
+ // Routing directive — v0.5 升级为判别联合(review 借鉴 1)
386
+ //
387
+ // Executor agent 在输出里 emit fenced JSON / structured block,phonon 解析后
388
+ // 升级成内部动作。新增 feedback / reply / done 三种语义,比单一 route 表达力强。
389
+ //
390
+ // kind 缺省时回退为 workflow.route(向后兼容 0.4 行为)。
391
+ // 兼容前缀:```phonon.workflow.* / ```workflow.*。
392
+ // ---------------------------------------------------------------------------
393
+
394
+ /** route:派新任务给目标 worker(v0.4 既有语义)。 */
395
+ export const WorkflowRouteDirective = z.object({
396
+ kind: z.literal("workflow.route"),
397
+ to: z.union([WorkflowNodeId, z.array(WorkflowNodeId).min(1)]),
398
+ message: z.string(),
399
+ reason: z.string().optional(),
400
+ metadata: z.record(z.unknown()).optional(),
401
+ });
402
+ export type WorkflowRouteDirective = z.infer<typeof WorkflowRouteDirective>;
403
+
404
+ /** feedback:让目标 worker 基于上次输出返工修订(显式语义,区分于新任务)。 */
405
+ export const WorkflowFeedbackDirective = z.object({
406
+ kind: z.literal("workflow.feedback"),
407
+ to: WorkflowNodeId,
408
+ message: z.string(),
409
+ reason: z.string().optional(),
410
+ metadata: z.record(z.unknown()).optional(),
411
+ });
412
+ export type WorkflowFeedbackDirective = z.infer<typeof WorkflowFeedbackDirective>;
413
+
414
+ /** reply:模拟键盘输入(应答目标 agent 卡住的交互提示,如 [Y/n])。 */
415
+ export const WorkflowReplyDirective = z.object({
416
+ kind: z.literal("workflow.reply"),
417
+ to: WorkflowNodeId,
418
+ /** 应答内容(通常 1-2 字符:"Y"/"n"/"y" 等)。 */
419
+ keystroke: z.string(),
420
+ reason: z.string().optional(),
421
+ });
422
+ export type WorkflowReplyDirective = z.infer<typeof WorkflowReplyDirective>;
423
+
424
+ /** done:显式宣告 workflow 完成。 */
425
+ export const WorkflowDoneDirective = z.object({
426
+ kind: z.literal("workflow.done"),
427
+ /** 最终总结文本,作为 workflow.finalText。 */
428
+ finalSummary: z.string().optional(),
429
+ reason: z.string().optional(),
430
+ });
431
+ export type WorkflowDoneDirective = z.infer<typeof WorkflowDoneDirective>;
432
+
433
+ export const WorkflowRoutingDirective = z.discriminatedUnion("kind", [
434
+ WorkflowRouteDirective,
435
+ WorkflowFeedbackDirective,
436
+ WorkflowReplyDirective,
437
+ WorkflowDoneDirective,
438
+ ]);
439
+ export type WorkflowRoutingDirective = z.infer<typeof WorkflowRoutingDirective>;