ai_co_discuss 0.3.93 → 0.3.95
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/.agents/skills/discuss-host/references/free.md +3 -1
- package/.agents/skills/workflow-creator/SKILL.md +2 -2
- package/backend/free_room.py +155 -59
- package/backend/identity.py +2 -2
- package/backend/server.py +2 -2
- package/docs/en/runner.md +3 -1
- package/docs/en/workflow.md +1 -1
- package/docs/runner.md +4 -2
- package/docs/workflow.md +1 -1
- package/package.json +1 -1
- package/runner/adapters/api.mjs +5 -4
- package/runner/adapters/profiles.mjs +11 -0
- package/runner/discuss.py +39 -3
- package/runner/discussion_api.mjs +1 -8
- package/runner/discussion_launch.mjs +2 -2
- package/runner/free_runner.mjs +10 -7
- package/runner/scheduler.mjs +12 -7
- package/web/app.js +6 -4
- package/web/modern.css +1 -1
- package/web/modern.js +11 -11
- package/workflows/story-serial/SKILL.md +11 -11
- package/workflows/story-serial/agents/critic.md +2 -5
- package/workflows/story-serial/agents/critic_people.md +1 -1
package/runner/adapters/api.mjs
CHANGED
|
@@ -54,8 +54,8 @@ export function participant_tools(context) {
|
|
|
54
54
|
tool('read_message', '读取指定原文;服务端按本次请求限制讨论中的可见范围', { id: NUMBER }, ['id']),
|
|
55
55
|
...file_tools(context.project), ...command_tools(context.commands, context.project)];
|
|
56
56
|
const answer = { text: { ...TEXT, maxLength: 32000 }, summary: { ...TEXT, maxLength: 1000 }, reply_to: NUMBER };
|
|
57
|
-
const submit = tool('submit_chat', '保存本次执行的完整回答并结束;summary 仅在回答较长时填写;只有主持人可用 to 点名成员,派出的工作做完或失败都会交回你(只点名一位、回答已足以直接给用户时可加 no_handoff: true,失败仍交回);to 点名多位并加 sequential: true 时按点名顺序依次接力、最后交回你,依次接力走完一圈算一轮,再加 loop: true
|
|
58
|
-
{ ...answer, ...(context.role === 'host' ? { to: { type: 'array', items: TEXT }, sequential: { type: 'boolean' }, loop: { type: 'boolean' }, no_handoff: { type: 'boolean' }, continue: { ...TEXT, maxLength: 300 },
|
|
57
|
+
const submit = tool('submit_chat', '保存本次执行的完整回答并结束;summary 仅在回答较长时填写;只有主持人可用 to 点名成员,派出的工作做完或失败都会交回你(只点名一位、回答已足以直接给用户时可加 no_handoff: true,失败仍交回);to 点名多位并加 sequential: true 时按点名顺序依次接力、最后交回你,依次接力走完一圈算一轮,再加 loop: true 则交回为检查点;parallel 列出要同一棒同时做的几组成员(每组至少两位、在 to 中相邻),这一组全部结束才交下一棒,组里有人失败则接力停下交回你;detach 列出与这次派活同时开工、但不等待的成员(不计入交回或下一棒),它失败时程序通知你;主持人要在房间空闲后接着推进时填 continue(写明继续做什么),算本链一轮;派活时可用 need_rounds(本任务从这次派活起还需要的轮数)加 rounds_reason 向用户申请轮数,超过本链剩余时任务链立即停下等用户批准',
|
|
58
|
+
{ ...answer, ...(context.role === 'host' ? { to: { type: 'array', items: TEXT }, sequential: { type: 'boolean' }, loop: { type: 'boolean' }, parallel: { type: 'array', items: { type: 'array', items: TEXT } }, detach: { type: 'array', items: TEXT }, no_handoff: { type: 'boolean' }, continue: { ...TEXT, maxLength: 300 },
|
|
59
59
|
need_rounds: { type: 'integer', minimum: 1, maximum: 100 }, rounds_reason: { ...TEXT, maxLength: 300 },
|
|
60
60
|
next_review: TEXT, review_scope: { ...TEXT, maxLength: 2000 } } : {}) }, ['text']);
|
|
61
61
|
const discussion_host = context.role === 'host' && context.discussion_request?.purpose === 'discuss_host';
|
|
@@ -144,8 +144,9 @@ function write_action(context, name, args) {
|
|
|
144
144
|
to: context.role === 'host' ? args.to || [] : [],
|
|
145
145
|
...(context.role === 'host' && typeof args.continue === 'string' && args.continue.trim() ? { continue: args.continue } : {}),
|
|
146
146
|
...(context.role === 'host' && args.no_handoff === true ? { no_handoff: true } : {}),
|
|
147
|
+
...(context.role === 'host' && Array.isArray(args.detach) && args.detach.length ? { detach: args.detach } : {}),
|
|
147
148
|
...(context.role === 'host' && Number.isInteger(args.need_rounds) ? { need_rounds: args.need_rounds, rounds_reason: args.rounds_reason } : {}),
|
|
148
|
-
...(context.role === 'host' && args.sequential === true ? { sequential: true, ...(args.loop === true ? { loop: true } : {}) } : {}),
|
|
149
|
+
...(context.role === 'host' && args.sequential === true ? { sequential: true, ...(args.loop === true ? { loop: true } : {}), ...(Array.isArray(args.parallel) && args.parallel.length ? { parallel: args.parallel } : {}) } : {}),
|
|
149
150
|
...(context.role === 'host' && args.next_review ? { next_review: args.next_review, review_scope: args.review_scope, review_roster: args.review_roster } : {}),
|
|
150
151
|
}, terminal: true };
|
|
151
152
|
const actions = {
|
|
@@ -258,7 +259,7 @@ export function system_prompt(context) {
|
|
|
258
259
|
下文是本项目的参与技能${host ? '与主持技能' : ''},与本地软件参与者使用同一份。你经本机接入程序以 API 参与,环境差异如下,与技能冲突时以本段为准:
|
|
259
260
|
- 只能使用提供的${context.project ? '房间工具和项目文件工具' : '房间工具'},没有${command_tools(context.commands, context.project).length ? '通用' : ''}命令行、浏览器、附件下载或其他软件。接入程序负责执行认领、续租和凭据管理;你的房间写入工具都绑定本次执行,不要重试结果不明的写入。
|
|
260
261
|
- 命令与工具对应:room view → read_room;message list / view → read_messages / read_message;message post --kind chat → submit_chat。${host ? 'state decision → read_decision(按任务的决策编号读取完整判断)。' : ''}${discussion && host
|
|
261
|
-
? '主持讨论工具还包括 state set → save_state;room update → update_topic;turn create --plan-file → arrange_turns;turn continue → continue_plan;turn roster → read_roster,读后 turn ask → ask_interjection;取消安排 → cancel_turn;room limits --done → finish_discussion / declare_done。' : ''}${host ? ' 派活用 to,派出的工作结束后都会交回你;房间空闲后要接着推进时填 continue;message post --sequential / --loop → submit_chat 的 sequential / loop;sequence view → read_sequence;sequence continue → continue_sequence。' : ''}${goal_host(context) ? ' goal view → read_goal;goal start → start_goal;goal done / block → finish_goal。' : ''}
|
|
262
|
+
? '主持讨论工具还包括 state set → save_state;room update → update_topic;turn create --plan-file → arrange_turns;turn continue → continue_plan;turn roster → read_roster,读后 turn ask → ask_interjection;取消安排 → cancel_turn;room limits --done → finish_discussion / declare_done。' : ''}${host ? ' 派活用 to,派出的工作结束后都会交回你;房间空闲后要接着推进时填 continue;message post --sequential / --loop / --with / --detach → submit_chat 的 sequential / loop / parallel / detach;sequence view → read_sequence;sequence continue → continue_sequence。' : ''}${goal_host(context) ? ' goal view → read_goal;goal start → start_goal;goal done / block → finish_goal。' : ''}
|
|
262
263
|
${project_text(context.project, context.commands)}
|
|
263
264
|
- ${max_requests(context) ? `本次执行最多 ${max_requests(context)} 次模型请求,每轮都计入;` : ''}互不依赖的读取或修改(如同一文件的多处 edit)在同一轮一次调用多个工具,减少模型请求次数。
|
|
264
265
|
- 必须通过写入工具保存真实结果,普通文本输出不会保存。确实无法继续时不调用写入工具直接结束,本次执行会暂停等待用户。
|
|
@@ -36,6 +36,17 @@
|
|
|
36
36
|
* - `task_rules(cli, {runs_commands})`: fixed task text this software needs, added by the runner so no member prompt drops it.
|
|
37
37
|
*/
|
|
38
38
|
const ALL_ACCESS = ['read', 'write', 'auto', 'full'];
|
|
39
|
+
/** API slot defaults; identity and wire protocol stay independent, and a custom slot starts without a provider. */
|
|
40
|
+
export const API_PROVIDERS = {
|
|
41
|
+
Codex: { label: 'OpenAI', base_url: 'https://api.openai.com/v1', api_protocol: 'responses', models: ['gpt-6.1-sol', 'gpt-6-astra', 'gpt-6-luna'] },
|
|
42
|
+
Claude: { label: 'Anthropic', base_url: 'https://api.anthropic.com/v1', api_protocol: 'anthropic', models: ['claude-sonnet-5-5', 'claude-opus-5-5', 'claude-haiku-5-5', 'claude-fable-5-1'] },
|
|
43
|
+
Antigravity: { label: 'Google', base_url: 'https://generativelanguage.googleapis.com/v1beta', api_protocol: 'gemini', models: ['gemini-4-argon', 'gemini-3.8-flash'] },
|
|
44
|
+
DeepSeek: { label: 'DeepSeek', base_url: 'https://api.deepseek.com/v1', api_protocol: 'chat', models: ['deepseek-flash', 'deepseek-v4-pro'] },
|
|
45
|
+
Qwen: { label: '阿里云百炼', base_url: 'https://dashscope.aliyuncs.com/compatible-mode/v1', api_protocol: 'chat', models: ['qwen3.8-flash', 'qwen3.7-plus', 'qwen3.8-max'] },
|
|
46
|
+
Grok: { label: 'xAI', base_url: 'https://api.x.ai/v1', api_protocol: 'responses', models: ['grok-4.7'] },
|
|
47
|
+
OpenAI: { label: 'OpenAI', base_url: 'https://api.openai.com/v1', api_protocol: 'responses', models: ['gpt-6.1-sol', 'gpt-6-astra', 'gpt-6-luna'] },
|
|
48
|
+
Custom: { label: '自定义 API', base_url: '', api_protocol: 'chat', models: [] },
|
|
49
|
+
};
|
|
39
50
|
/** Model requests one model-API execution may make unless the room sets its own; 0 means no limit, up to the maximum. */
|
|
40
51
|
export const DEFAULT_API_MAX_REQUESTS = 300;
|
|
41
52
|
export const MAX_API_MAX_REQUESTS = 1000;
|
package/runner/discuss.py
CHANGED
|
@@ -28,9 +28,9 @@ MAX_TEXT_LENGTH = 32000
|
|
|
28
28
|
MAX_SUMMARY_LENGTH = 1000
|
|
29
29
|
MAX_BRIEF_LENGTH = 1600
|
|
30
30
|
MAX_MEMBER_HANDLE = 80
|
|
31
|
-
PARTICIPANTS = ("用户", "Codex", "Claude", "Antigravity", "DeepSeek", "Qwen", "Grok")
|
|
31
|
+
PARTICIPANTS = ("用户", "Codex", "Claude", "Antigravity", "DeepSeek", "Qwen", "Grok", "OpenAI", "Custom")
|
|
32
32
|
# A platform is a type; `Claude-review` is a second Claude instance with its own session. Mirrors backend/identity.py.
|
|
33
|
-
INSTANCE_PATTERN = re.compile(r"(Codex|Claude|Antigravity|DeepSeek|Qwen|Grok)(?:-[0-9A-Za-z_]{1,16})?")
|
|
33
|
+
INSTANCE_PATTERN = re.compile(r"(Codex|Claude|Antigravity|DeepSeek|Qwen|Grok|OpenAI|Custom)(?:-[0-9A-Za-z_]{1,16})?")
|
|
34
34
|
KINDS = ("opinion", "moderation", "chat")
|
|
35
35
|
OVERVIEW_FIELDS = ("id", "title", "goal", "host", "phase", "focus", "message_count",
|
|
36
36
|
"latest_id", "state_until", "current_turn", "queued_turns", "held_since", "limits", "completed_rounds", "answered_turns", "duplicate", "submission_id",
|
|
@@ -98,6 +98,29 @@ class RequestPlan:
|
|
|
98
98
|
download_file: str | None = None
|
|
99
99
|
|
|
100
100
|
|
|
101
|
+
class JoinPreviousStep(argparse.Action):
|
|
102
|
+
"""`--with NAME`: append NAME to `--to` and remember that it shares the step of the member named just before it."""
|
|
103
|
+
|
|
104
|
+
def __call__(self, parser, namespace, value, option_string=None):
|
|
105
|
+
names = list(getattr(namespace, self.dest) or [])
|
|
106
|
+
if not names:
|
|
107
|
+
parser.error("--with 须跟在一个 --to 之后")
|
|
108
|
+
names.append(value)
|
|
109
|
+
setattr(namespace, self.dest, names)
|
|
110
|
+
namespace.joined_steps = [*getattr(namespace, "joined_steps", []), len(names) - 1]
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def parallel_groups(names, joined):
|
|
114
|
+
"""The `parallel` field from the `--to` list and the positions `--with` joined to the step before them."""
|
|
115
|
+
groups = []
|
|
116
|
+
for index in sorted(joined):
|
|
117
|
+
if groups and groups[-1][-1] == names[index - 1]:
|
|
118
|
+
groups[-1].append(names[index])
|
|
119
|
+
else:
|
|
120
|
+
groups.append([names[index - 1], names[index]])
|
|
121
|
+
return groups
|
|
122
|
+
|
|
123
|
+
|
|
101
124
|
def positive_int(value):
|
|
102
125
|
"""Parse a positive page or resource identifier."""
|
|
103
126
|
try:
|
|
@@ -112,7 +135,7 @@ def positive_int(value):
|
|
|
112
135
|
def ai_name(value):
|
|
113
136
|
"""Accept an AI platform or a named instance of one, such as Claude-review."""
|
|
114
137
|
if not INSTANCE_PATTERN.fullmatch(value):
|
|
115
|
-
raise argparse.ArgumentTypeError("须为 Codex/Claude/Antigravity/DeepSeek/Qwen/Grok,或平台名-后缀(后缀 1–16 个英文字母、数字或下划线)")
|
|
138
|
+
raise argparse.ArgumentTypeError("须为 Codex/Claude/Antigravity/DeepSeek/Qwen/Grok/OpenAI/Custom,或平台名-后缀(后缀 1–16 个英文字母、数字或下划线)")
|
|
116
139
|
return value
|
|
117
140
|
|
|
118
141
|
|
|
@@ -289,6 +312,10 @@ def message_commands(subcommands, registry):
|
|
|
289
312
|
help="自由房间主持人:--to 点名的多位成员按点名顺序依次接力,走完一圈算一轮,最后一位做完交回你")
|
|
290
313
|
post.add_argument("--loop", action="store_true",
|
|
291
314
|
help="与 --sequential 同用:交回成为检查点,用 sequence continue 开始下一圈,不继续即直接验收")
|
|
315
|
+
post.add_argument("--detach", type=member_handle, action="append", default=[], metavar="MEMBER",
|
|
316
|
+
help="自由房间主持人:与 --to 同用,点名一位与这次派活同时开工、但不等它的成员(不计入交回或下一棒的等待);它失败时程序通知你;可重复")
|
|
317
|
+
post.add_argument("--with", dest="to", type=member_handle, action=JoinPreviousStep, metavar="MEMBER",
|
|
318
|
+
help="与 --sequential 同用:点名一位成员,与紧挨在前的 --to(或 --with)同一棒、同时做;这一组全部结束才交下一棒,组里有人失败则接力停下交回你;可重复")
|
|
292
319
|
post.add_argument("--need-rounds", type=positive_int, metavar="N",
|
|
293
320
|
help="自由房间主持人:与 --to 同用,声明本任务从这次派活起还需要的轮数(须带 --rounds-reason);超过本链剩余轮数时任务链立即停下,请用户一键批准差额")
|
|
294
321
|
post.add_argument("--rounds-reason", metavar="REASON", help="与 --need-rounds 同用:为什么需要这些轮(≤300 字),展示给用户")
|
|
@@ -678,10 +705,19 @@ def plan_message(args):
|
|
|
678
705
|
payload["rounds_reason"] = args.rounds_reason
|
|
679
706
|
if args.loop and not args.sequential:
|
|
680
707
|
raise CliError("--loop 须与 --sequential 一起使用")
|
|
708
|
+
if args.detach:
|
|
709
|
+
if not args.to:
|
|
710
|
+
raise CliError("--detach 须与 --to 一起使用")
|
|
711
|
+
payload["detach"] = args.detach
|
|
712
|
+
joined = getattr(args, "joined_steps", [])
|
|
713
|
+
if joined and not args.sequential:
|
|
714
|
+
raise CliError("--with 须与 --sequential 一起使用")
|
|
681
715
|
if args.sequential:
|
|
682
716
|
if len(args.to) < 2:
|
|
683
717
|
raise CliError("--sequential 须用 --to 按顺序点名至少两位成员")
|
|
684
718
|
payload["sequential"] = True
|
|
719
|
+
if joined:
|
|
720
|
+
payload["parallel"] = parallel_groups(args.to, joined)
|
|
685
721
|
if args.loop:
|
|
686
722
|
payload["loop"] = True
|
|
687
723
|
for field in ("next_review", "review_scope", "review_roster"):
|
|
@@ -3,14 +3,7 @@ import { randomUUID } from 'node:crypto';
|
|
|
3
3
|
import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
|
|
4
4
|
import { join } from 'node:path';
|
|
5
5
|
|
|
6
|
-
export
|
|
7
|
-
Codex: { label: 'OpenAI', base_url: 'https://api.openai.com/v1', api_protocol: 'responses', models: ['gpt-6.1-sol', 'gpt-6-astra', 'gpt-6-luna'] },
|
|
8
|
-
Claude: { label: 'Anthropic', base_url: 'https://api.anthropic.com/v1', api_protocol: 'anthropic', models: ['claude-sonnet-5-5', 'claude-opus-5-5', 'claude-haiku-5-5', 'claude-fable-5-1'] },
|
|
9
|
-
Antigravity: { label: 'Google', base_url: 'https://generativelanguage.googleapis.com/v1beta', api_protocol: 'gemini', models: ['gemini-4-argon', 'gemini-3.8-flash'] },
|
|
10
|
-
DeepSeek: { label: 'DeepSeek', base_url: 'https://api.deepseek.com/v1', api_protocol: 'chat', models: ['deepseek-flash', 'deepseek-v4-pro'] },
|
|
11
|
-
Qwen: { label: '阿里云百炼', base_url: 'https://dashscope.aliyuncs.com/compatible-mode/v1', api_protocol: 'chat', models: ['qwen3.8-flash', 'qwen3.7-plus', 'qwen3.8-max'] },
|
|
12
|
-
Grok: { label: 'xAI', base_url: 'https://api.x.ai/v1', api_protocol: 'responses', models: ['grok-4.7'] },
|
|
13
|
-
};
|
|
6
|
+
export { API_PROVIDERS } from './adapters/profiles.mjs';
|
|
14
7
|
const PROTOCOLS = ['responses', 'chat', 'anthropic', 'gemini'];
|
|
15
8
|
const REQUEST_TIMEOUT_MS = 180_000;
|
|
16
9
|
const MAX_OUTPUT_TOKENS = 8192;
|
|
@@ -28,7 +28,7 @@ import { assert_shared_home, CONNECTION_FIELDS, configuration_snapshot, read_reg
|
|
|
28
28
|
|
|
29
29
|
// A platform is a type; `Claude-review` is another Claude instance with its own session, model and effort. An ID is
|
|
30
30
|
// English letters, digits and underscores only; a Chinese name is a display label.
|
|
31
|
-
const INSTANCE_PATTERN = /^(Codex|Claude|Antigravity|DeepSeek|Qwen|Grok)(?:-[0-9A-Za-z_]{1,16})?$/;
|
|
31
|
+
const INSTANCE_PATTERN = /^(Codex|Claude|Antigravity|DeepSeek|Qwen|Grok|OpenAI|Custom)(?:-[0-9A-Za-z_]{1,16})?$/;
|
|
32
32
|
|
|
33
33
|
/** The platform of an AI instance name, or null. */
|
|
34
34
|
export function platform_of(name) { return typeof name === 'string' ? INSTANCE_PATTERN.exec(name)?.[1] ?? null : null; }
|
|
@@ -539,7 +539,7 @@ const MAX_PRESET_LABEL = 32;
|
|
|
539
539
|
function english_preset_ids(presets) {
|
|
540
540
|
const taken = new Set(presets.map(preset => preset.name));
|
|
541
541
|
return presets.map(preset => {
|
|
542
|
-
const platform = /^(Codex|Claude|Antigravity|DeepSeek|Qwen|Grok)(?:-|$)/.exec(preset.name || '')?.[1];
|
|
542
|
+
const platform = /^(Codex|Claude|Antigravity|DeepSeek|Qwen|Grok|OpenAI|Custom)(?:-|$)/.exec(preset.name || '')?.[1];
|
|
543
543
|
if (!platform || platform_of(preset.name)) return preset;
|
|
544
544
|
let number = 1;
|
|
545
545
|
while (taken.has(`${platform}-preset${number}`)) number++;
|
package/runner/free_runner.mjs
CHANGED
|
@@ -25,6 +25,8 @@ export const GOAL_IDLE_MS = 30_000;
|
|
|
25
25
|
// A manual member's session that refuses a delivery is retried in the same execution, after a wait the owner can cut short.
|
|
26
26
|
const MAX_DELIVERY_RETRIES = 3;
|
|
27
27
|
const RETRY_DELAY_SECONDS = 60;
|
|
28
|
+
// The cause is gone once the restarted runner is back, so its retry needs no long wait.
|
|
29
|
+
const RESTART_RETRY_DELAY_SECONDS = 5;
|
|
28
30
|
const MAX_RETRY_REASON = 500;
|
|
29
31
|
const LIVE = ['reserving', 'delivering', 'sidecar_pending', 'running', 'retry_wait'];
|
|
30
32
|
// The longest private chat reply the site keeps.
|
|
@@ -912,9 +914,10 @@ export class FreeRoomRunner {
|
|
|
912
914
|
}
|
|
913
915
|
// A restart between persisting and invoking cannot prove non-delivery, so it never resends.
|
|
914
916
|
if (row.status === 'delivering') return this.fail(member.name, execution.id, '上次投递结果不明,未自动重发;请核对原会话后重新发送');
|
|
915
|
-
// Inherited from a runner that stopped or crashed: once its software processes are gone, no answer can come
|
|
917
|
+
// Inherited from a runner that stopped or crashed: once its software processes are gone, no answer can come, so the
|
|
918
|
+
// same execution is delivered again after the retry wait instead of handing the failure to the host.
|
|
916
919
|
if (row.status === 'running' && this.orphan(execution.id)?.alive === false) {
|
|
917
|
-
return this.
|
|
920
|
+
return this.retry_or_fail(member.name, execution, '本机接入程序重启或异常退出,这次执行的软件进程已结束,未提交回答', { restarted: true });
|
|
918
921
|
}
|
|
919
922
|
if (row.error) return this.retry_or_fail(member.name, execution, row.error);
|
|
920
923
|
if (this.exited.has(execution.id)) {
|
|
@@ -1073,19 +1076,19 @@ export class FreeRoomRunner {
|
|
|
1073
1076
|
}
|
|
1074
1077
|
/**
|
|
1075
1078
|
* A manual member's session refused the delivery, or the software failed before any turn ran: keep the execution and
|
|
1076
|
-
* deliver again after a wait shown on the website, up to MAX_DELIVERY_RETRIES times;
|
|
1077
|
-
* attempt, fails as before.
|
|
1079
|
+
* deliver again after a wait shown on the website, up to MAX_DELIVERY_RETRIES times; so is an execution whose software
|
|
1080
|
+
* ended with the previous runner process (`restarted`). Anything else, or the last attempt, fails as before.
|
|
1078
1081
|
*/
|
|
1079
|
-
async retry_or_fail(name, execution, reason) {
|
|
1082
|
+
async retry_or_fail(name, execution, reason, { restarted = false } = {}) {
|
|
1080
1083
|
const attempt = (execution.retry?.attempt || 0) + 1;
|
|
1081
1084
|
const not_started = this.not_started.delete(execution.id);
|
|
1082
1085
|
if (this.not_retryable.delete(execution.id)) return this.fail(name, execution.id, reason);
|
|
1083
|
-
if (this.config.participants[name]?.project_access !== 'own' && !not_started) return this.fail(name, execution.id, reason);
|
|
1086
|
+
if (!restarted && this.config.participants[name]?.project_access !== 'own' && !not_started) return this.fail(name, execution.id, reason);
|
|
1084
1087
|
if (attempt > MAX_DELIVERY_RETRIES) return this.fail(name, execution.id, `${reason}(已自动重试 ${MAX_DELIVERY_RETRIES} 次)`);
|
|
1085
1088
|
this.stop_member(name);
|
|
1086
1089
|
this.exited.delete(execution.id);
|
|
1087
1090
|
await api(this.config, `/executions/${execution.id}`, { action: 'retry_wait', attempt,
|
|
1088
|
-
retry_at: Math.floor(Date.now() / 1000) + RETRY_DELAY_SECONDS, reason: reason.slice(0, MAX_RETRY_REASON) });
|
|
1091
|
+
retry_at: Math.floor(Date.now() / 1000) + (restarted ? RESTART_RETRY_DELAY_SECONDS : RETRY_DELAY_SECONDS), reason: reason.slice(0, MAX_RETRY_REASON) });
|
|
1089
1092
|
this.save(name, { status: 'retry_wait', error: '' });
|
|
1090
1093
|
}
|
|
1091
1094
|
/** Record the cause on the website, then stop this member's own processes. */
|
package/runner/scheduler.mjs
CHANGED
|
@@ -48,7 +48,7 @@ export const REVIEW_PROMPT_VERSION = 1;
|
|
|
48
48
|
export const REVIEW_CONFIDENCE = 0.85;
|
|
49
49
|
export const MAX_REVIEW_INPUT = 60000;
|
|
50
50
|
/** A loop's checkpoint: another pass costs every member a run, so going on needs the review threshold; ending only wakes the host. */
|
|
51
|
-
export const SEQUENCE_PROMPT_VERSION =
|
|
51
|
+
export const SEQUENCE_PROMPT_VERSION = 2;
|
|
52
52
|
export const SEQUENCE_CONFIDENCE = 0.85;
|
|
53
53
|
const HOST_CHOICE = '__host__';
|
|
54
54
|
const NOTE_CHOICE = '__note__';
|
|
@@ -397,15 +397,20 @@ export async function decide_review_completion(home, scheduler, ready, fetch_imp
|
|
|
397
397
|
export async function decide_sequence_checkpoint(home, scheduler, sequence, fetch_impl) {
|
|
398
398
|
const fallback = reason => ({ action: 'host', confidence: null, label: reason });
|
|
399
399
|
const answers_of_pass = sequence.answers || [];
|
|
400
|
-
const
|
|
400
|
+
const last_step = answers_of_pass.at(-1)?.step;
|
|
401
|
+
// The last step may be a group working at once: all of its answers carry the pass's conclusion.
|
|
402
|
+
const lasts = last_step === undefined ? answers_of_pass.slice(-1) : answers_of_pass.filter(item => item.step === last_step);
|
|
401
403
|
if (sequence.status !== 'checkpoint' || !['pending', 'delivering'].includes(sequence.handoff_status)) return fallback('检查点已处理或循环已结束,交给主持人');
|
|
402
|
-
if (!sequence.origin?.text?.trim() || !
|
|
404
|
+
if (!sequence.origin?.text?.trim() || !lasts.length || lasts.some(item => !item.text?.trim())) return fallback('缺少主持人安排或本圈回答原文,交给主持人');
|
|
403
405
|
if (typeof sequence.remaining_rounds === 'number' && sequence.remaining_rounds < 1) return fallback('任务链剩余轮数不足,交给主持人');
|
|
404
|
-
const
|
|
406
|
+
const step_name = step => Array.isArray(step) ? `${step.join('、')}(同时)` : step;
|
|
407
|
+
const first = [sequence.order.find(Boolean)].flat().join('、');
|
|
408
|
+
const last = { member: lasts.map(item => item.member).join('、') };
|
|
409
|
+
const group = lasts.length > 1;
|
|
405
410
|
const questions = [{ type: 'choice', name: 'checkpoint',
|
|
406
|
-
instructions: `主持人安排成员按固定顺序(${sequence.order.join(' → ')})循环接力,第 ${sequence.pass}
|
|
407
|
-
{ value: 'continue', description: `再走一圈:${last.member} 明确要求返工,或指出必须修改后才能采用的问题,下一圈由 ${first} 按这些意见修改` },
|
|
408
|
-
{ value: HOST_CHOICE, description: `交给主持人:${last.member} 认可当前结果,或只剩可以不采纳的建议;需要主持人或用户裁定取舍;成员之间有分歧;回答没有明确结论` }] }];
|
|
411
|
+
instructions: `主持人安排成员按固定顺序(${sequence.order.map(step_name).join(' → ')})循环接力,第 ${sequence.pass} 圈已走完。根据本圈最后一${group ? '棒(' : '位('}${last.member}${group ? ',同时完成' : ''})回答里表达的结论,判断是否按主持人的安排再走一圈。只识别回答表达的结论,不自行评价工作质量,不选择成员,不改变顺序。`, choices: [
|
|
412
|
+
{ value: 'continue', description: `再走一圈:${last.member}${group ? ' 中任一位' : ''} 明确要求返工,或指出必须修改后才能采用的问题,下一圈由 ${first} 按这些意见修改` },
|
|
413
|
+
{ value: HOST_CHOICE, description: `交给主持人:${last.member}${group ? ' 都' : ''} 认可当前结果,或只剩可以不采纳的建议;需要主持人或用户裁定取舍;成员之间有分歧;回答没有明确结论` }] }];
|
|
409
414
|
const state = JSON.stringify({ arrangement: sequence.origin, order: sequence.order, pass: sequence.pass,
|
|
410
415
|
answers: answers_of_pass.map(({ member, text }) => ({ member, text })) });
|
|
411
416
|
if (JSON.stringify({ state, questions }).length > MAX_REVIEW_INPUT) return fallback('本圈回答原文超过 60000 字符,交给主持人');
|
package/web/app.js
CHANGED
|
@@ -81,13 +81,15 @@ const SOURCE_LABELS = { demo: "示例", user: "本人发言", proxy: "用户代
|
|
|
81
81
|
const TURN_LABELS = { pending: "待处理", notified: "已通知,待回答", answered: "已回答", cancelled: "已取消" };
|
|
82
82
|
const PLATFORM_IDENTITIES = {
|
|
83
83
|
"用户": { label: "User", initials: "U", style: "user", icon: "/icons/user.svg", description: "发起与协助" },
|
|
84
|
-
Codex: { initials: "CX", style: "codex", icon: "/icons/openai.svg", description: "AI 身份" },
|
|
84
|
+
Codex: { initials: "CX", style: "codex", icon: "/icons/openai.svg", api_label: "OpenAI API", description: "AI 身份" },
|
|
85
85
|
Claude: { initials: "CL", style: "claude", icon: "/icons/claude.svg", description: "AI 身份" },
|
|
86
86
|
// The platform ID is Antigravity (the software); screens show its nickname Gemini.
|
|
87
87
|
Antigravity: { label: "Gemini", initials: "GM", style: "gemini", icon: "/icons/gemini.svg", description: "AI 身份" },
|
|
88
88
|
DeepSeek: { initials: "DS", style: "deepseek", icon: "/icons/deepseek.svg", description: "AI 身份" },
|
|
89
89
|
Qwen: { initials: "QW", style: "qwen", icon: "/icons/qwen.svg", description: "AI 身份" },
|
|
90
90
|
Grok: { initials: "GK", style: "grok", description: "AI 身份" },
|
|
91
|
+
OpenAI: { initials: "OA", style: "codex", icon: "/icons/openai.svg", description: "AI 身份" },
|
|
92
|
+
Custom: { label: "自定义 API", initials: "API", style: "custom", api_label: "自定义 API", description: "AI 身份" },
|
|
91
93
|
};
|
|
92
94
|
// An instance shows its platform's nickname with its own suffix: Antigravity-2 → Gemini-2.
|
|
93
95
|
const IDENTITIES = by_platform(PLATFORM_IDENTITIES, (identity, name) => ({ ...identity, label: identity.label ? identity.label + name.slice(platform_of(name).length) : name, description: "AI 实例" }));
|
|
@@ -149,11 +151,11 @@ const PLATFORM_CLIENTS = { Codex: [{ value: "codex_cli", label: "Codex CLI" }, {
|
|
|
149
151
|
Claude: [{ value: "claude", label: "Claude Code CLI" }],
|
|
150
152
|
Antigravity: [{ value: "antigravity_cli", label: "Antigravity CLI" }, { value: "antigravity", label: "Antigravity App" }] };
|
|
151
153
|
for (const name of Object.keys(PLATFORM_IDENTITIES).filter(name => name !== "用户")) {
|
|
152
|
-
(PLATFORM_CLIENTS[name] ||= []).push({ value: "api", label:
|
|
154
|
+
(PLATFORM_CLIENTS[name] ||= []).push({ value: "api", label: PLATFORM_IDENTITIES[name].api_label || `${PLATFORM_IDENTITIES[name].label || name} API` });
|
|
153
155
|
}
|
|
154
156
|
// A platform is a type; `Claude-review` is another instance of it. Lookups by instance name fall back to the
|
|
155
157
|
// platform, while iteration still lists the platforms only.
|
|
156
|
-
const INSTANCE_PATTERN = /^(Codex|Claude|Antigravity|DeepSeek|Qwen|Grok)(?:-[0-9A-Za-z_]{1,16})?$/;
|
|
158
|
+
const INSTANCE_PATTERN = /^(Codex|Claude|Antigravity|DeepSeek|Qwen|Grok|OpenAI|Custom)(?:-[0-9A-Za-z_]{1,16})?$/;
|
|
157
159
|
/** The platform of an AI instance name, or null for the user and invalid names. */
|
|
158
160
|
function platform_of(name) { return INSTANCE_PATTERN.exec(String(name))?.[1] ?? null; }
|
|
159
161
|
/** Resolve instance names to their platform's entry without listing instances as keys. */
|
|
@@ -3242,7 +3244,7 @@ function write_launch_field(name, field, value) {
|
|
|
3242
3244
|
else state.launch.profiles[name][field] = value;
|
|
3243
3245
|
if (field === "adapter" && value === "api") {
|
|
3244
3246
|
const profile = state.launch.profiles[name], preset = state.launch.api_providers?.[platform_of(name)];
|
|
3245
|
-
if (preset && !profile.base_url) Object.assign(profile, { base_url: preset.base_url, api_protocol: preset.api_protocol, model: preset.models[0] });
|
|
3247
|
+
if (preset && !profile.base_url) Object.assign(profile, { base_url: preset.base_url, api_protocol: preset.api_protocol, model: preset.models[0] || "" });
|
|
3246
3248
|
for (const key of ["effort", "session_id", "project_access", "project_id", "executable", "source_thread_id", "ipc_directory", "allowed_tools"]) delete profile[key];
|
|
3247
3249
|
}
|
|
3248
3250
|
if (field === "base_url") delete state.launch.profiles[name].api_key_ref;
|