ai_co_discuss 0.2.99 → 0.3.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 (39) hide show
  1. package/.agents/skills/discuss-host/SKILL.md +3 -3
  2. package/.agents/skills/discuss-host/references/discussion.md +16 -16
  3. package/.agents/skills/discuss-host/references/launch.md +1 -3
  4. package/.agents/skills/discuss-host/references/manual.md +14 -77
  5. package/.agents/skills/discuss-participant/SKILL.md +3 -3
  6. package/.agents/skills/discuss-participant/references/discussion.md +29 -27
  7. package/.agents/skills/discuss-participant/references/free.md +2 -2
  8. package/.agents/skills/discuss-participant/references/manual.md +8 -10
  9. package/.agents/skills/discuss-participant/references/writing.md +9 -9
  10. package/.github/SECURITY.md +1 -1
  11. package/README.md +3 -3
  12. package/README.zh-CN.md +3 -3
  13. package/backend/free_room.py +360 -68
  14. package/backend/room_import.py +75 -15
  15. package/backend/runtime.py +81 -169
  16. package/backend/schema.sql +136 -244
  17. package/backend/server.py +602 -166
  18. package/backend/session_room.py +919 -0
  19. package/docs/architecture.md +3 -2
  20. package/docs/en/architecture.md +4 -3
  21. package/docs/en/runner.md +3 -1
  22. package/docs/en/workflow.md +9 -6
  23. package/docs/runner.md +3 -1
  24. package/docs/workflow.md +11 -8
  25. package/package.json +2 -2
  26. package/runner/api_participant.mjs +47 -44
  27. package/runner/discuss.py +77 -58
  28. package/runner/discussion_adapters.mjs +6 -3
  29. package/runner/discussion_launch.mjs +98 -70
  30. package/runner/discussion_runner.mjs +38 -496
  31. package/runner/discussion_service.mjs +21 -74
  32. package/runner/discussion_sidecar.mjs +4 -13
  33. package/runner/expert_prompts.mjs +29 -21
  34. package/runner/free_runner.mjs +84 -19
  35. package/runner/session_driver.mjs +175 -0
  36. package/web/app.js +193 -128
  37. package/web/backup.js +1 -1
  38. package/web/modern.css +1 -1
  39. package/web/modern.js +5482 -6646
package/runner/discuss.py CHANGED
@@ -196,6 +196,8 @@ def room_commands(subcommands, registry):
196
196
  parser = add_leaf(subcommands, registry, f"room {action}", description, action != "list")
197
197
  if action == "list":
198
198
  parser.add_argument("--submission-id", type=submission_id, help="按建房提交编号精确查询")
199
+ elif action in ("view", "brief"):
200
+ parser.add_argument("--request", type=positive_int, help="手动成员:按自己的有效请求限制可读原文")
199
201
  limits = add_leaf(subcommands, registry, "room limits", "查看上限;--check 检查并执行到限处理;--done 通知程序目标已完成", True)
200
202
  limits.add_argument("--check", action="store_true")
201
203
  limits.add_argument("--done", action="store_true", help="主持人宣告目标已完成:结束讨论,房间回到自由协作;须带 --conclusion")
@@ -221,8 +223,10 @@ def message_commands(subcommands, registry):
221
223
  listing.add_argument("--mention", type=participant)
222
224
  listing.add_argument("--kind", choices=READ_KINDS, help="筛选观点、主持说明、协作消息或旧房间的历史回执")
223
225
  listing.add_argument("--submission-id", type=submission_id, help="按提交编号精确查询结果")
226
+ listing.add_argument("--request", type=positive_int, help="手动成员:按自己的有效请求限制可读原文")
224
227
  view = add_leaf(subcommands, registry, "message view", "读取一条精确原文与来源", True)
225
228
  view.add_argument("id", type=positive_int, help="消息编号")
229
+ view.add_argument("--request", type=positive_int, help="手动成员:按自己的有效请求限制可读原文")
226
230
  post = add_leaf(subcommands, registry, "message post", "提交观点、主持说明、协作回答或代发", True)
227
231
  add_text(post, "body")
228
232
  post.add_argument("--attachment", action="append", type=attachment_id, default=[],
@@ -230,8 +234,7 @@ def message_commands(subcommands, registry):
230
234
  add_text(post, "summary")
231
235
  post.add_argument("--summary-by", type=participant, help="摘要撰写者:原作者或实际操作者;默认操作者")
232
236
  post.add_argument("--author", type=participant, help="原作者;默认等于 --as;不同即代发")
233
- post.add_argument("--kind", choices=KINDS, default="opinion")
234
- post.add_argument("--turn", type=positive_int, help="所回答的安排编号,仅 opinion 可用;省略时回答你当前有效的安排")
237
+ post.add_argument("--kind", choices=("chat", "moderation"), default="chat")
235
238
  post.add_argument("--execution", type=positive_int, help="自由房间:本次执行编号,仅 chat 可用;执行失效后不能改作普通发言")
236
239
  post.add_argument("--to", type=ai_name, action="append", default=[],
237
240
  help="自由房间:主持人点名派活,普通成员提出回应建议(须房主确认);可重复")
@@ -261,15 +264,15 @@ def attachment_commands(subcommands, registry):
261
264
 
262
265
 
263
266
  def turn_commands(subcommands, registry):
264
- """Keep planning, notification text, and delivery confirmation distinct."""
267
+ """Read and control discussion plans through their dedicated operations."""
265
268
  descriptions = {"list": "分页读取安排历史", "current": "读取阶段和最近安排;需核对状态及专家",
266
- "view": "读取一个安排", "prompt": "生成简短通知;不发送、不标记送达",
269
+ "view": "读取一个安排",
267
270
  "roster": "申请最新专家状态(在场专家、本轮待发言者与 revision);整轮安排前必做",
268
271
  "order": "读取发言顺序(已发言、正在发言、待发言与 revision);调整顺序前必做",
269
272
  "reorder": "房主调整尚未发言者的顺序(须带 turn order 的 --revision);已发言与正在发言者不动",
270
- "create": "安排一位专家,或用 --plan-file 一次安排整轮(须带 --roster);不发送通知",
273
+ "create": "安排一位专家,或用 --plan-file 一次安排整轮(须带最新 --roster)",
271
274
  "continue": "处理用户操作或打断后,按原排队继续下一位",
272
- "mark-notified": "实际送达后显式确认", "cancel": "取消指定未完成安排"}
275
+ "cancel": "取消指定未完成安排"}
273
276
  for action, description in descriptions.items():
274
277
  parser = add_leaf(subcommands, registry, f"turn {action}", description, True)
275
278
  if action == "list":
@@ -281,18 +284,21 @@ def turn_commands(subcommands, registry):
281
284
  parser.add_argument("--expert", type=ai_name, help="单个安排的专家;与 --plan-file 二选一")
282
285
  add_text(parser, "question")
283
286
  parser.add_argument("--plan-file", help='整轮安排 JSON 数组 [{"expert":"Claude","question":"…"}],按顺序接力;- 从标准输入读取')
284
- parser.add_argument("--roster", help="turn roster 返回的 revision;整轮安排(--plan-file)必填,专家状态变化后需重新获取")
287
+ parser.add_argument("--roster", required=True, help="turn roster 返回的 revision;任何新安排前必读,专家状态变化后需重新获取")
285
288
  parser.add_argument("--submission-id", type=submission_id, required=True,
286
289
  help="新安排唯一编号;重试沿用同编号和内容")
287
290
  if action == "reorder":
288
291
  parser.add_argument("--revision", required=True, help="turn order 返回的 revision;顺序变化后需重新获取")
289
292
  parser.add_argument("--member", action="append", type=ai_name, required=True,
290
293
  help="按新顺序重复指定,须恰好包含 turn order 中 movable 的全部成员")
294
+ if action == "continue":
295
+ parser.add_argument("--roster", required=True, help="turn roster 返回的 revision;状态变化后须重新读取")
291
296
 
292
297
 
293
298
  def state_commands(subcommands, registry):
294
299
  """Register separately requested host summaries with explicit cutoffs."""
295
- add_leaf(subcommands, registry, "state view", "读取主持摘要及材料截止编号", True)
300
+ view = add_leaf(subcommands, registry, "state view", "读取主持摘要及材料截止编号", True)
301
+ view.add_argument("--request", type=positive_int, help="手动成员:按自己的有效请求限制可读原文")
296
302
  parser = add_leaf(subcommands, registry, "state set", "保存主持摘要;不自动推定已读范围", True)
297
303
  add_text(parser, "body", required=True)
298
304
  add_text(parser, "brief")
@@ -300,24 +306,24 @@ def state_commands(subcommands, registry):
300
306
 
301
307
 
302
308
  def run_commands(subcommands, registry):
303
- """Expose mode and lifecycle controls independently of discussion phases."""
304
- for action in ("view", "configure", "start", "pause", "stop"):
305
- parser = add_leaf(subcommands, registry, f"run {action}", "查看或显式控制讨论运行", True)
309
+ """Expose discussion-session lifecycle controls independently of the resident service."""
310
+ for action in ("view", "start", "pause", "resume", "stop"):
311
+ parser = add_leaf(subcommands, registry, f"run {action}", "查看或显式控制讨论会话", True)
306
312
  if action != "view":
307
- parser.add_argument("--revision", type=non_negative_int, required=True)
308
- parser.add_argument("--mode", choices=("manual", "automatic"))
313
+ parser.add_argument("--revision", required=True, help="run view 返回的 revision;状态变化后须重新读取")
309
314
  if action == "start":
310
- parser.add_argument("--deadline", type=non_negative_int, help="截止时间 Unix 秒数;0 表示不限时长,仍受话题上限约束;省略时使用话题上限")
311
- parser.add_argument("--phase", choices=("initial", "discussing"))
315
+ add_text(parser, "goal")
316
+ for name in ("max-rounds", "max-minutes", "execution-minutes"):
317
+ parser.add_argument(f"--{name}", type=non_negative_int, default=argparse.SUPPRESS)
318
+ parser.add_argument("--submission-id", type=submission_id,
319
+ help="可选启动提交编号;重试须沿用编号和原请求")
312
320
 
313
321
 
314
322
  def execution_commands(subcommands, registry):
315
- """Enter one delivered task; duplicate enters cannot start duplicate work."""
316
- for action in ("view", "enter", "fail"):
317
- parser = add_leaf(subcommands, registry, f"execution {action}", "读取、进入或报告本次执行失败", True)
323
+ """Read an execution or report its failure through its durable identifier."""
324
+ for action in ("view", "fail"):
325
+ parser = add_leaf(subcommands, registry, f"execution {action}", "读取执行或报告本次执行失败", True)
318
326
  parser.add_argument("id", type=positive_int)
319
- if action == "enter":
320
- parser.add_argument("--session-id", help="实际软件会话 ID,默认从受限任务文件读取")
321
327
  if action == "fail":
322
328
  parser.add_argument("--reason", required=True)
323
329
 
@@ -429,6 +435,23 @@ def topic_payload(args):
429
435
  return payload
430
436
 
431
437
 
438
+ def read_context_params(args):
439
+ """Select an explicit manual request or the execution bound to this access file."""
440
+ request_id = getattr(args, "request", None)
441
+ execution_value = load_access(args).get("execution_id")
442
+ if request_id is not None and execution_value is not None:
443
+ raise CliError("读取时 --request 不能与凭据文件中的 execution_id 同用")
444
+ if request_id is not None:
445
+ return {"request_id": request_id}
446
+ if execution_value is None:
447
+ return {}
448
+ try:
449
+ execution_id = positive_int(str(execution_value))
450
+ except argparse.ArgumentTypeError as error:
451
+ raise CliError("凭据文件中的 execution_id 无效") from error
452
+ return {"execution_id": execution_id}
453
+
454
+
432
455
  def plan_room(args):
433
456
  """Translate room operations to one existing HTTP endpoint."""
434
457
  if args.action == "list":
@@ -459,23 +482,24 @@ def plan_room(args):
459
482
  return RequestPlan(base + "/conclusion")
460
483
  if args.action == "protocol":
461
484
  return RequestPlan(f"/r/{args.room}/protocol")
462
- return RequestPlan(base, view="brief" if args.action == "brief" else "overview")
485
+ return RequestPlan(base, view="brief" if args.action == "brief" else "overview", params=read_context_params(args))
463
486
 
464
487
 
465
488
  def plan_message(args):
466
489
  """Preserve operator, original author, message role, and replay identifier."""
467
490
  base = f"/api/rooms/{args.room}/messages"
468
491
  if args.action == "view":
469
- return RequestPlan(f"{base}/{args.id}")
492
+ return RequestPlan(f"{base}/{args.id}", params=read_context_params(args))
470
493
  if args.action == "list":
471
494
  params = {"page": args.page, "q": args.query, "author": args.author, "mention": args.mention,
472
- "after": args.after, "kind": args.kind, "submission_id": args.submission_id}
495
+ "after": args.after, "kind": args.kind, "submission_id": args.submission_id,
496
+ **read_context_params(args)}
473
497
  return RequestPlan(base, params={key: value for key, value in params.items() if value is not None})
474
498
  actor = require_actor(args)
475
- if args.turn and args.kind != "opinion":
476
- raise CliError("只有 opinion 可以关联 --turn")
477
- if (args.execution or args.request or args.to) and (args.kind != "chat" or args.turn):
478
- raise CliError("--execution / --request 仅用于自由房间的 chat 回答,不能与 --turn 同时使用")
499
+ if (args.to or args.handoff) and args.kind != "chat":
500
+ raise CliError("--to / --handoff 仅用于 chat 消息")
501
+ if args.kind == "moderation" and not (args.execution or args.request):
502
+ raise CliError("主持说明须关联本次 --execution 或有效 --request")
479
503
  if args.execution and args.request:
480
504
  raise CliError("--execution 用于接入程序交付的执行,--request 用于手动接入成员,二者不能同用")
481
505
  payload = {"author": args.author or actor, "submitted_by": actor, "source": "cli",
@@ -493,8 +517,6 @@ def plan_message(args):
493
517
  payload["summary"] = summary
494
518
  if args.summary_by:
495
519
  payload["summary_by"] = args.summary_by
496
- if args.turn:
497
- payload["turn_id"] = args.turn
498
520
  if args.execution:
499
521
  payload["execution_id"] = args.execution
500
522
  if args.request:
@@ -557,12 +579,12 @@ def plan_turn(args):
557
579
  if args.action == "create":
558
580
  return RequestPlan(base + "/turns", "POST", turn_payload(args))
559
581
  if args.action == "continue":
560
- return RequestPlan(base + "/turns/continue", "POST", {"actor": require_actor(args)})
582
+ return RequestPlan(base + "/turns/continue", "POST", {"actor": require_actor(args), "revision": args.roster})
561
583
  path = f"{base}/turns/{args.id}"
562
- if args.action in ("mark-notified", "cancel"):
563
- status = "notified" if args.action == "mark-notified" else "cancelled"
584
+ if args.action == "cancel":
585
+ status = "cancelled"
564
586
  return RequestPlan(path, "POST", {"actor": require_actor(args), "status": status})
565
- return RequestPlan(path, view="prompt" if args.action == "prompt" else None)
587
+ return RequestPlan(path)
566
588
 
567
589
 
568
590
  def turn_payload(args):
@@ -578,17 +600,17 @@ def turn_payload(args):
578
600
  raise CliError(f"无效 JSON: {error}") from error
579
601
  if not isinstance(turns, list) or not all(isinstance(item, dict) for item in turns):
580
602
  raise CliError("--plan-file 必须是由 expert、question 对象组成的数组")
581
- return {**payload, "turns": turns, **({"roster": args.roster} if args.roster else {})}
603
+ return {**payload, "turns": turns, "roster": args.roster}
582
604
  if not args.expert or question is None:
583
605
  raise CliError("需要 --expert 与 --question/--question-file,或改用 --plan-file")
584
- return {**payload, "expert": args.expert, "question": question}
606
+ return {**payload, "expert": args.expert, "question": question, "roster": args.roster}
585
607
 
586
608
 
587
609
  def plan_state(args):
588
610
  """Keep a host summary's cutoff explicit instead of assuming latest means read."""
589
611
  base = f"/api/rooms/{args.room}"
590
612
  if args.action == "view":
591
- return RequestPlan(base, view="state")
613
+ return RequestPlan(base, view="state", params=read_context_params(args))
592
614
  if args.until < 0:
593
615
  raise CliError("--until 不能小于 0")
594
616
  payload = {"actor": require_actor(args), "text": text_field(args, "body"), "until": args.until}
@@ -615,28 +637,30 @@ def plan_api(args):
615
637
 
616
638
 
617
639
  def plan_run(args):
618
- """Build one versioned lifecycle operation without starting a daemon."""
619
- path = f"/api/rooms/{args.room}/runtime"
640
+ """Build one revision-bound discussion-session lifecycle request."""
641
+ path = f"/api/rooms/{args.room}/discussion"
620
642
  if args.action == "view":
621
643
  return RequestPlan(path)
622
- body = {"action": args.action, "revision": args.revision}
623
- for key in ("mode", "deadline", "phase"):
624
- if getattr(args, key, None) is not None:
625
- body[key] = getattr(args, key)
644
+ body = {"action": "end" if args.action == "stop" else args.action, "revision": args.revision}
645
+ if args.action == "start":
646
+ goal = text_field(args, "goal")
647
+ if goal is not None:
648
+ body["goal"] = goal
649
+ limits = {key: getattr(args, key) for key in ("max_rounds", "max_minutes", "execution_minutes")
650
+ if hasattr(args, key)}
651
+ if limits:
652
+ body["limits"] = limits
653
+ if args.submission_id:
654
+ body["submission_id"] = args.submission_id
626
655
  return RequestPlan(path, "POST", body)
627
656
 
628
657
 
629
658
  def plan_execution(args):
630
- """Use the bound actual session when acknowledging a delivered execution."""
631
- body = {"action": args.action}
632
- if args.action == "enter":
633
- access = load_access(args)
634
- body["session_id"] = args.session_id or access.get("session_id")
635
- if not body["session_id"]:
636
- raise CliError("尚未取得实际 session_id,请先核对接入回执")
637
- if args.action == "fail":
638
- body["reason"] = args.reason
639
- return RequestPlan(f"/api/rooms/{args.room}/events/{args.id}", "POST", body)
659
+ """Read or fail one durable member execution."""
660
+ path = f"/api/rooms/{args.room}/executions/{args.id}"
661
+ if args.action == "view":
662
+ return RequestPlan(path)
663
+ return RequestPlan(path, "POST", {"action": "fail", "reason": args.reason})
640
664
 
641
665
 
642
666
  def plan_member(args):
@@ -785,11 +809,6 @@ def project_result(value, plan, site):
785
809
  result["guidance"] = GUIDANCE.get(value["phase"], "按本次主持或专家职责,结合最新阶段、安排和必要原文决定下一步。")
786
810
  result["url"] = f"{site}/r/{value['id']}"
787
811
  return result
788
- if plan.view == "prompt":
789
- entry = f"{site}/r/{value['room_id']}?participant={value['expert']}"
790
- prompt = f"请 {value['expert']} 回应:{value['question']}\n安排编号:{value['id']}\n按需阅读:{entry}\n回答附简短摘要及完整正文,保留关键条件和不确定性。"
791
- return {"turn_id": value["id"], "status": value["status"], "prompt": prompt,
792
- "delivery": "未发送;仅生成文字,实际通知后使用 turn mark-notified"}
793
812
  return value
794
813
 
795
814
 
@@ -810,7 +829,7 @@ def command_schema(args, registry):
810
829
  item = {"name": action.dest, "flags": action.option_strings,
811
830
  "required": action.required, "help": action.help}
812
831
  if action.dest == "actor" and name in ("room create", "room update", "message post",
813
- "turn create", "turn mark-notified", "turn cancel", "state set", "attachment upload"):
832
+ "turn create", "turn cancel", "state set", "attachment upload"):
814
833
  item["required"] = True
815
834
  if action.choices is not None:
816
835
  item["choices"] = list(action.choices)
@@ -542,7 +542,7 @@ export function claude_window(session_id, env = process.env) {
542
542
  return null;
543
543
  }
544
544
 
545
- /** Whether this execution goes into a taken-over member's own open window rather than a background process. */
545
+ /** Whether this execution goes into a manual member's own open window (taken over with `own` permissions) rather than a background process. */
546
546
  export function claude_inbox_ready(profile, binding) {
547
547
  return profile.adapter === 'claude' && profile.project_access === 'own' && Boolean(profile.session_id)
548
548
  && binding.session_id === profile.session_id && Boolean(claude_window(profile.session_id)?.inbox);
@@ -573,6 +573,8 @@ async function claude_inbox_delivery(config, inbox, task) {
573
573
  });
574
574
  socket.on('error', error => written ? undefined : reject(not_sent(error)));
575
575
  });
576
+ // The discussion runner enters the execution once the session is known, as it does when a process reports it.
577
+ task.on_binding?.(config.session_id);
576
578
  const marker = `${task.id}.task.json`;
577
579
  setTimeout(() => {
578
580
  if (!claude_session_mentions(config.session_id, marker)) {
@@ -878,6 +880,7 @@ async function cli_delivery(config, binding, task, spawn_process, user_home) {
878
880
 
879
881
  /** Queue one immutable request for the official App-managed Sidecar. */
880
882
  async function antigravity_delivery(config, binding, task) {
883
+ if (!task.id.startsWith('free-')) throw new Error('Sidecar 只支持自由协作执行请求,旧讨论事件已停用');
881
884
  const directory = resolve(config.ipc_directory);
882
885
  try {
883
886
  mkdirSync(directory, { recursive: true });
@@ -892,8 +895,8 @@ async function antigravity_delivery(config, binding, task) {
892
895
  } catch (error) { throw not_sent(error); }
893
896
  const path = join(directory, `${task.id}.request.json`);
894
897
  if (existsSync(path)) throw new Error('Sidecar 请求已存在;必须核对回执,不自动重复发送');
895
- atomic_json(path, { id: task.id, mode: task.id.startsWith('free-') ? 'free' : 'brainstorm',
896
- event_id: Number(task.id.replace(/^(event|free)-/, '')), prompt: task.prompt,
898
+ atomic_json(path, { id: task.id, mode: 'free',
899
+ event_id: Number(task.id.replace(/^free-/, '')), prompt: task.prompt,
897
900
  session_id: binding.session_id || '', access_path: task.access_path });
898
901
  return { session_id: binding.session_id || '', status: 'sidecar_pending' };
899
902
  }