ai_co_discuss 0.4.2 → 0.4.4
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/workflow-creator/SKILL.md +1 -1
- package/README.md +1 -1
- package/backend/free_room.py +40 -16
- package/backend/private_chat.py +6 -3
- package/backend/schema.sql +6 -1
- package/backend/server.py +2714 -2713
- package/docs/runner.md +4 -4
- package/docs/workflow.md +1 -1
- package/package.json +1 -1
- package/runner/adapters/profiles.mjs +4 -3
- package/runner/free_runner.mjs +72 -21
- package/web/app.js +9 -1
- package/web/backup.js +1 -1
- package/web/modern.css +1 -1
- package/web/modern.js +37 -37
|
@@ -52,7 +52,7 @@ description: AI CoDiscuss 工作流的创建与修改技能。用户要把房间
|
|
|
52
52
|
templates/ 等(可选) 主持人要用的模板、参考资料,只认文本文件
|
|
53
53
|
```
|
|
54
54
|
|
|
55
|
-
**流程画布**(`flow.json
|
|
55
|
+
**流程画布**(`flow.json`):用户在工作流面板里看到的示意图,帮用户一眼看懂 SKILL.md 里谁接谁。它只是示意:程序不执行它,主持人也收不到它,派活只按 SKILL.md。所以流程照常完整写在 SKILL.md,图照着 SKILL.md 画,两边对不上时以 SKILL.md 为准,并把图改对。
|
|
56
56
|
|
|
57
57
|
```json
|
|
58
58
|
{ "nodes": [
|
package/README.md
CHANGED
|
@@ -30,7 +30,7 @@ Most multi-agent frameworks spin up fresh agents inside their own runtime. AI Co
|
|
|
30
30
|
- **Structured discussions** — set a goal and round / time limits; the host schedules speakers and follow-ups, then the room returns to free collaboration with history and conclusions kept.
|
|
31
31
|
- **Goal mode** — give the host an objective with `/goal`; whenever the room goes idle the host is woken to carry on, until it confirms the goal is complete, gets blocked, a budget runs out or you end it.
|
|
32
32
|
- **Smart Scheduling** — optionally let a decision model handle routine routing instead of waking the host.
|
|
33
|
-
- **Workflows** — save a way of working as a host playbook (`SKILL.md`) plus a set of agent cards and share it as a folder. Manage them (view, create, import, edit, delete)
|
|
33
|
+
- **Workflows** — save a way of working as a host playbook (`SKILL.md`) plus a set of agent cards and share it as a folder. Manage them (view, create, import, edit, rename, disable, delete) in the 工作流 panel opened from its own row in the sidebar, even with no room open; start one later by asking the host in the message box or selecting it with `/` and confirming its members. Next to its `SKILL.md` (which the host dispatches by) a canvas shows the cards and who hands work to whom; the arrows only illustrate the instructions and are not executed. A workflow brings the method and the people, not a guaranteed procedure. See [runner docs](docs/runner.md#工作流可选).
|
|
34
34
|
- **Mix any participant** — local CLIs and desktop apps, model APIs and manually connected sessions in one room.
|
|
35
35
|
- **Shared workspace** — point a room at a project directory, grant each local agent read-only or writable access, and browse files with text, Markdown and static HTML previews from the room.
|
|
36
36
|
- **Live roster** — invite, pause, recall or remove members at any time, including resuming an existing agent session.
|
package/backend/free_room.py
CHANGED
|
@@ -51,6 +51,10 @@ ROOM_EVENT_PAGE = 500
|
|
|
51
51
|
# and answer sit under that answer; later messages (the user asking something meanwhile) then stay above it.
|
|
52
52
|
ANCHOR_SQL = """CASE WHEN r.purpose='handoff' THEN COALESCE((SELECT MAX(a.answer_id) FROM member_requests a
|
|
53
53
|
WHERE a.room_id=r.room_id AND a.batch_id=r.message_id AND a.purpose<>'handoff'), r.message_id) ELSE r.message_id END"""
|
|
54
|
+
# The room's latest message when a request (aliased `r`) was made. A relay step dispatched from an answer that sits in an
|
|
55
|
+
# older message's slot, after the user posted meanwhile, goes under the latest top-level message rather than back up there.
|
|
56
|
+
DISPATCHED_AFTER_SQL = """(SELECT MAX(m.id) FROM messages m WHERE m.room_id=r.room_id AND m.id>=r.message_id
|
|
57
|
+
AND m.id<=COALESCE(r.answer_id,m.id) AND CAST(strftime('%s',m.created_at) AS INTEGER)<=r.created_at)"""
|
|
54
58
|
# Rounds used and the owner's additions in the room's recent chains, for "第 2/3 轮"; parameter: the room.
|
|
55
59
|
# A round is a host dispatch (a batch) or a continue the host asked for (`free_host_wakes` with a `continue:` key); a
|
|
56
60
|
# relay's later steps in the same pass (`free_relay_steps`) ride on its first, so a whole pass is one round.
|
|
@@ -143,7 +147,7 @@ SESSION_MODES = ("new", "borrowed")
|
|
|
143
147
|
FREE_OPERATIONS = ("messages", "attachments", "move", "folder", "rename", "archive", "restore", "delete")
|
|
144
148
|
MEMBER_FIELDS = ("id", "name", "adapter", "role", "status", "access", "session_mode", "read_until")
|
|
145
149
|
EXECUTION_FIELDS = ("id", "member_id", "status", "session_id", "lease_until", "created_at", "finished_at", "result",
|
|
146
|
-
"activity", "activity_at")
|
|
150
|
+
"activity", "activity_at", "paused_at")
|
|
147
151
|
# What the runner found when a voided execution's software was to stop; '' until it reports.
|
|
148
152
|
STOP_RESULTS = ("stopped", "unstoppable", "unconfirmed")
|
|
149
153
|
# How long a member voided where the runner could not stop it gets no new delivery unless its late answer shows the turn ended.
|
|
@@ -154,24 +158,28 @@ REWIND_EFFECTS = ("cut", "fresh", "kept")
|
|
|
154
158
|
CAPABILITY_COLUMNS = """(SELECT stoppable FROM execution_capabilities c WHERE c.execution_id=e.id) AS stoppable,
|
|
155
159
|
(SELECT progress FROM execution_capabilities c WHERE c.execution_id=e.id) AS progress,
|
|
156
160
|
(SELECT stop_result FROM execution_capabilities c WHERE c.execution_id=e.id) AS stop_result,
|
|
157
|
-
(SELECT steer FROM execution_capabilities c WHERE c.execution_id=e.id) AS steer
|
|
161
|
+
(SELECT steer FROM execution_capabilities c WHERE c.execution_id=e.id) AS steer,
|
|
162
|
+
(SELECT resume FROM execution_capabilities c WHERE c.execution_id=e.id) AS resume"""
|
|
158
163
|
|
|
159
164
|
|
|
160
165
|
def reported_capabilities(row):
|
|
161
|
-
"""`{stoppable, progress, steer}` as the runner reported them for one execution, or None before it did (or an older runner).
|
|
162
|
-
`steer`: the owner can add an interjection the running software takes without ending its turn.
|
|
166
|
+
"""`{stoppable, progress, steer, resume}` as the runner reported them for one execution, or None before it did (or an older runner).
|
|
167
|
+
`steer`: the owner can add an interjection the running software takes without ending its turn. `resume`: the runner
|
|
168
|
+
can stop the software's turn and continue the same session, so the owner may pause the execution without ending it."""
|
|
163
169
|
if row["stoppable"] is None:
|
|
164
170
|
return None
|
|
165
|
-
return {"stoppable": bool(row["stoppable"]), "progress": bool(row["progress"]), "steer": bool(row["steer"])
|
|
171
|
+
return {"stoppable": bool(row["stoppable"]), "progress": bool(row["progress"]), "steer": bool(row["steer"]),
|
|
172
|
+
"resume": bool(row["resume"])}
|
|
166
173
|
# Columns added after the first free-room tables; Python databases created by that build gain them here.
|
|
167
174
|
LATER_COLUMNS = {"room_members": {"access": "TEXT NOT NULL DEFAULT 'runner'", "session_mode": "TEXT NOT NULL DEFAULT 'new'"},
|
|
168
175
|
"member_executions": {"changes": "TEXT NOT NULL DEFAULT ''", "edited": "TEXT NOT NULL DEFAULT ''", "cwd": "TEXT NOT NULL DEFAULT ''",
|
|
169
|
-
"activity": "TEXT NOT NULL DEFAULT ''", "activity_at": "INTEGER NOT NULL DEFAULT 0"
|
|
176
|
+
"activity": "TEXT NOT NULL DEFAULT ''", "activity_at": "INTEGER NOT NULL DEFAULT 0",
|
|
177
|
+
"paused_at": "INTEGER NOT NULL DEFAULT 0"},
|
|
170
178
|
"member_requests": {"chain_id": "INTEGER NOT NULL DEFAULT 0", "batch_id": "INTEGER NOT NULL DEFAULT 0",
|
|
171
179
|
"purpose": "TEXT NOT NULL DEFAULT 'ask'", "created_by": "TEXT NOT NULL DEFAULT 'user'"},
|
|
172
180
|
"free_chain_holds": {"asked_rounds": "INTEGER NOT NULL DEFAULT 0", "asked_reason": "TEXT NOT NULL DEFAULT ''"},
|
|
173
181
|
"free_sequences": {"gates": "TEXT NOT NULL DEFAULT '[]'"},
|
|
174
|
-
"execution_capabilities": {"steer": "INTEGER"}}
|
|
182
|
+
"execution_capabilities": {"steer": "INTEGER", "resume": "INTEGER"}}
|
|
175
183
|
|
|
176
184
|
|
|
177
185
|
def migrate_free_tables(connection):
|
|
@@ -742,6 +750,7 @@ def recent_executions(connection, room_id):
|
|
|
742
750
|
(SELECT MAX(answer_id) FROM member_requests WHERE execution_id=e.id) AS answer_id,
|
|
743
751
|
(SELECT MIN(message_id) FROM member_requests WHERE execution_id=e.id) AS request_message_id,
|
|
744
752
|
(SELECT MIN({ANCHOR_SQL}) FROM member_requests r WHERE r.execution_id=e.id) AS anchor_message_id,
|
|
753
|
+
(SELECT MIN({DISPATCHED_AFTER_SQL}) FROM member_requests r WHERE r.execution_id=e.id) AS dispatched_after,
|
|
745
754
|
(SELECT COUNT(*) FROM execution_notes WHERE execution_id=e.id) AS note_count,
|
|
746
755
|
(SELECT denials FROM execution_denials WHERE execution_id=e.id) AS denials,{CAPABILITY_COLUMNS},
|
|
747
756
|
(SELECT json_object('created_at',created_at) FROM late_answers WHERE execution_id=e.id) AS late_answer
|
|
@@ -754,7 +763,7 @@ def recent_executions(connection, room_id):
|
|
|
754
763
|
executions.append({**public_execution(row), "member": row["member"], "cwd": row["cwd"], "changes": json.loads(row["changes"] or "[]"),
|
|
755
764
|
"changes_recorded": bool(row["changes"]), "request_ids": json.loads(row["request_ids"]),
|
|
756
765
|
"answer_id": row["answer_id"], "request_message_id": row["request_message_id"],
|
|
757
|
-
"anchor_message_id": row["anchor_message_id"], "requests": requests,
|
|
766
|
+
"anchor_message_id": row["anchor_message_id"], "dispatched_after": row["dispatched_after"], "requests": requests,
|
|
758
767
|
"notes": row["note_count"], "denials": json.loads(row["denials"] or "[]"),
|
|
759
768
|
"capabilities": reported_capabilities(row), "stop_result": row["stop_result"] or "",
|
|
760
769
|
"late_answer": json.loads(row["late_answer"]) if row["late_answer"] else None,
|
|
@@ -2551,13 +2560,16 @@ def add_interjection(connection, room_id, row, payload, now):
|
|
|
2551
2560
|
if earlier["execution_id"] != row["id"] or earlier["text"] != text:
|
|
2552
2561
|
raise FileExistsError("submission_id 已用于另一条插话")
|
|
2553
2562
|
return public_interjection(earlier, row["status"])
|
|
2554
|
-
capability = connection.execute("SELECT steer FROM execution_capabilities WHERE execution_id=?", (row["id"],)).fetchone()
|
|
2555
|
-
if not capability or not capability["steer"]:
|
|
2563
|
+
capability = connection.execute("SELECT steer,resume FROM execution_capabilities WHERE execution_id=?", (row["id"],)).fetchone()
|
|
2564
|
+
if not capability or not (capability["steer"] or capability["resume"]):
|
|
2556
2565
|
raise FileExistsError("这位成员的软件不支持执行中插话,可停止后带上这句话重问")
|
|
2557
2566
|
if connection.execute("SELECT COUNT(*) FROM execution_interjections WHERE execution_id=?", (row["id"],)).fetchone()[0] >= MAX_INTERJECTIONS:
|
|
2558
2567
|
raise FileExistsError(f"一次执行最多插话 {MAX_INTERJECTIONS} 条")
|
|
2559
2568
|
cursor = connection.execute("""INSERT INTO execution_interjections (execution_id,room_id,submission_id,text,created_at,updated_at)
|
|
2560
2569
|
VALUES (?,?,?,?,?,?)""", (row["id"], room_id, submission_id, text, now, now))
|
|
2570
|
+
# Software that cannot take it mid-turn hears it by a pause: the runner stops the turn and resumes with it.
|
|
2571
|
+
if not capability["steer"] and not row["paused_at"]:
|
|
2572
|
+
connection.execute("UPDATE member_executions SET paused_at=? WHERE id=?", (now, row["id"]))
|
|
2561
2573
|
return public_interjection(connection.execute("SELECT * FROM execution_interjections WHERE id=?", (cursor.lastrowid,)).fetchone(), row["status"])
|
|
2562
2574
|
|
|
2563
2575
|
|
|
@@ -2639,12 +2651,13 @@ def execution_action(connection, room_id, execution_id, token, payload):
|
|
|
2639
2651
|
# What this delivery can do, as the runner's adapter reported; a retried delivery may differ, so the latest counts.
|
|
2640
2652
|
if type(payload.get("stoppable")) is not bool or type(payload.get("progress")) is not bool:
|
|
2641
2653
|
raise ValueError("delivery 须带布尔值 stoppable 与 progress")
|
|
2642
|
-
# Older runners report no `steer`; their deliveries take no interjection.
|
|
2643
|
-
if type(payload.get("steer", False)) is not bool:
|
|
2644
|
-
raise ValueError("delivery 的 steer 须为布尔值")
|
|
2645
|
-
connection.execute("""INSERT INTO execution_capabilities (execution_id,room_id,stoppable,progress,steer) VALUES (
|
|
2646
|
-
ON CONFLICT(execution_id) DO UPDATE SET stoppable=excluded.stoppable,progress=excluded.progress,steer=excluded.steer
|
|
2647
|
-
|
|
2654
|
+
# Older runners report no `steer` or `resume`; their deliveries take no interjection and no pause.
|
|
2655
|
+
if type(payload.get("steer", False)) is not bool or type(payload.get("resume", False)) is not bool:
|
|
2656
|
+
raise ValueError("delivery 的 steer 与 resume 须为布尔值")
|
|
2657
|
+
connection.execute("""INSERT INTO execution_capabilities (execution_id,room_id,stoppable,progress,steer,resume) VALUES (?,?,?,?,?,?)
|
|
2658
|
+
ON CONFLICT(execution_id) DO UPDATE SET stoppable=excluded.stoppable,progress=excluded.progress,steer=excluded.steer,
|
|
2659
|
+
resume=excluded.resume""", (execution_id, room_id, int(payload["stoppable"]), int(payload["progress"]),
|
|
2660
|
+
int(payload.get("steer", False)), int(payload.get("resume", False))))
|
|
2648
2661
|
elif action == "interjection":
|
|
2649
2662
|
# The runner's report on one interjection: it reached the software, or why it could not; a repeated report changes nothing.
|
|
2650
2663
|
record_interjection_result(connection, room_id, execution_id, payload, now)
|
|
@@ -2663,6 +2676,17 @@ def execution_action(connection, room_id, execution_id, token, payload):
|
|
|
2663
2676
|
(now + LEASE_SECONDS, activity, activity_at, execution_id))
|
|
2664
2677
|
connection.executemany("INSERT OR IGNORE INTO execution_notes (execution_id,seq,room_id,text,created_at) VALUES (?,?,?,?,?)",
|
|
2665
2678
|
[(execution_id, note["seq"], room_id, note["text"], now) for note in notes])
|
|
2679
|
+
elif action == "pause":
|
|
2680
|
+
# The owner's pause from the private chat: the runner stops the software's turn, the execution stays live and
|
|
2681
|
+
# resumes in the same session with the owner's next line. Ending the turn is the member's interrupt instead.
|
|
2682
|
+
capability = connection.execute("SELECT resume FROM execution_capabilities WHERE execution_id=?", (execution_id,)).fetchone()
|
|
2683
|
+
if not capability or not capability["resume"]:
|
|
2684
|
+
raise FileExistsError("这位成员的接入方式不能暂停后接着做;要结束这次执行请用停止")
|
|
2685
|
+
if not row["paused_at"]:
|
|
2686
|
+
connection.execute("UPDATE member_executions SET paused_at=? WHERE id=?", (now, execution_id))
|
|
2687
|
+
elif action == "resume":
|
|
2688
|
+
# The runner resumed the paused turn in the same session.
|
|
2689
|
+
connection.execute("UPDATE member_executions SET paused_at=0 WHERE id=?", (execution_id,))
|
|
2666
2690
|
elif action == "interject":
|
|
2667
2691
|
interjection = add_interjection(connection, room_id, row, payload, now)
|
|
2668
2692
|
detail = execution_detail(connection, connection.execute("SELECT * FROM member_executions WHERE id=?", (execution_id,)).fetchone())
|
package/backend/private_chat.py
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
Private chat: the owner talks to one room member directly, in the member's own session, outside the room's messages.
|
|
3
3
|
|
|
4
4
|
Each row of `private_chats` is one thing the owner said and the member's reply. While the member works on a room
|
|
5
|
-
execution that takes interjections, a new line goes in as an
|
|
5
|
+
execution that takes interjections (mid-turn, or by pausing and resuming its turn), a new line goes in as an
|
|
6
|
+
interjection of that execution; otherwise it waits until
|
|
6
7
|
the member is idle and the runner runs it as a turn of its own, taking the reply from the software's output. Only the
|
|
7
8
|
owner reads or writes chats; the runner, which holds the owner credential, reports their progress.
|
|
8
9
|
"""
|
|
@@ -67,9 +68,11 @@ def member_chat(connection, room_id, member_id, token, payload=None):
|
|
|
67
68
|
raise FileExistsError("submission_id 已用于另一句私聊")
|
|
68
69
|
return public_chat(connection, earlier)
|
|
69
70
|
now = int(time.time())
|
|
70
|
-
# A room execution that takes interjections hears it now; one that cannot
|
|
71
|
+
# A room execution that takes interjections (mid-turn, or by a pause and resume) hears it now; one that cannot
|
|
72
|
+
# waits, like a turn already running.
|
|
71
73
|
execution = connection.execute("""SELECT e.* FROM member_executions e JOIN execution_capabilities c ON c.execution_id=e.id
|
|
72
|
-
WHERE e.member_id=? AND e.status IN ('dispatched','started') AND c.steer=1 ORDER BY e.id DESC LIMIT 1""",
|
|
74
|
+
WHERE e.member_id=? AND e.status IN ('dispatched','started') AND (c.steer=1 OR c.resume=1) ORDER BY e.id DESC LIMIT 1""",
|
|
75
|
+
(member["id"],)).fetchone()
|
|
73
76
|
status, execution_id = "pending", None
|
|
74
77
|
if execution:
|
|
75
78
|
add_interjection(connection, room_id, execution, {"text": text, "submission_id": INTERJECTION_PREFIX + submission_id}, now)
|
package/backend/schema.sql
CHANGED
|
@@ -299,6 +299,9 @@ CREATE TABLE IF NOT EXISTS member_executions (
|
|
|
299
299
|
cwd TEXT NOT NULL DEFAULT '',
|
|
300
300
|
activity TEXT NOT NULL DEFAULT '',
|
|
301
301
|
activity_at INTEGER NOT NULL DEFAULT 0,
|
|
302
|
+
-- When the owner paused it from the private chat (0 = not paused): the runner stopped its software turn and resumes
|
|
303
|
+
-- the same execution in the same session with the owner's next line; the execution itself stays live.
|
|
304
|
+
paused_at INTEGER NOT NULL DEFAULT 0,
|
|
302
305
|
UNIQUE(room_id,client_id)
|
|
303
306
|
);
|
|
304
307
|
-- What the owner wrote about each member (strengths, division of work), by name so it survives leaving and rejoining.
|
|
@@ -445,7 +448,9 @@ CREATE TABLE IF NOT EXISTS execution_capabilities (
|
|
|
445
448
|
stoppable INTEGER CHECK(stoppable IN (0,1)),
|
|
446
449
|
progress INTEGER CHECK(progress IN (0,1)),
|
|
447
450
|
stop_result TEXT NOT NULL DEFAULT '' CHECK(stop_result IN ('','stopped','unstoppable','unconfirmed')),
|
|
448
|
-
steer INTEGER CHECK(steer IN (0,1))
|
|
451
|
+
steer INTEGER CHECK(steer IN (0,1)),
|
|
452
|
+
-- The runner can stop the software's turn and continue the same session, so a pause keeps the execution.
|
|
453
|
+
resume INTEGER CHECK(resume IN (0,1))
|
|
449
454
|
);
|
|
450
455
|
-- What the owner said into one running execution without ending it (an interjection from the private-chat page), and
|
|
451
456
|
-- whether the runner got it into the member's software: pending until the runner reports delivered or failed. A retry
|