VeryOnly123 commited on
Commit
06ef3e6
·
1 Parent(s): dcbc8e9
Files changed (11) hide show
  1. .gitignore +3 -0
  2. Dockerfile +35 -0
  3. app.py +48 -0
  4. config.json +4 -0
  5. outline.md +248 -0
  6. requirements.txt +1 -0
  7. run.py +79 -0
  8. settings.json +3 -0
  9. solver.py +194 -0
  10. spawn.py +85 -0
  11. static/solver.html +289 -0
.gitignore ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ workspaces/
2
+ *.pyc
3
+ __pycache__/
Dockerfile ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM node:18-slim
2
+
3
+ # Install Python3, pip
4
+ RUN apt-get update && apt-get install -y python3 python3-pip && rm -rf /var/lib/apt/lists/*
5
+
6
+ WORKDIR /app
7
+
8
+ # Install Python dependencies
9
+ COPY requirements.txt .
10
+ RUN pip3 install --no-cache-dir -r requirements.txt
11
+
12
+ # Global install Claude Code CLI
13
+ RUN npm install -g @anthropic-ai/claude-code
14
+
15
+ # Copy solver source files
16
+ COPY run.py spawn.py outline.md config.json /app/solver/
17
+
18
+ # Copy web app files
19
+ COPY app.py solver.py /app/
20
+ COPY static/ /app/static/
21
+
22
+ # Create workspaces directory
23
+ RUN mkdir -p /app/workspaces
24
+
25
+ # Claude Code config
26
+ RUN mkdir -p /root/.claude
27
+ COPY settings.json /root/.claude/settings.json
28
+
29
+ # Environment variables (set via Hugging Face Space secrets)
30
+ ENV ANTHROPIC_BASE_URL=""
31
+ ENV ANTHROPIC_API_KEY=""
32
+
33
+ EXPOSE 7860
34
+
35
+ CMD ["python3", "/app/app.py"]
app.py ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask import Flask, request, jsonify, send_from_directory
2
+ import solver
3
+
4
+ app = Flask(__name__, static_folder='static')
5
+
6
+
7
+ @app.route('/')
8
+ def index():
9
+ return send_from_directory('static', 'solver.html')
10
+
11
+
12
+ @app.route('/api/submit', methods=['POST'])
13
+ def api_submit():
14
+ data = request.json
15
+ problem = data.get('problem', '').strip()
16
+ if not problem:
17
+ return jsonify({'error': 'No problem provided'}), 400
18
+
19
+ task_id = solver.submit_task(problem)
20
+ if task_id is None:
21
+ return jsonify({'error': 'Server is busy, please try again later'}), 503
22
+
23
+ return jsonify({'task_id': task_id})
24
+
25
+
26
+ @app.route('/api/status/<task_id>')
27
+ def api_status(task_id):
28
+ task = solver.get_task(task_id)
29
+ if not task:
30
+ return jsonify({'error': 'Task not found'}), 404
31
+ return jsonify(task)
32
+
33
+
34
+ @app.route('/api/result/<task_id>')
35
+ def api_result(task_id):
36
+ content = solver.get_task_result(task_id)
37
+ if content is None:
38
+ return jsonify({'error': 'Result not available yet'}), 404
39
+ return jsonify({'content': content})
40
+
41
+
42
+ @app.route('/api/tasks')
43
+ def api_tasks():
44
+ return jsonify({'tasks': solver.list_tasks()})
45
+
46
+
47
+ if __name__ == '__main__':
48
+ app.run(host='0.0.0.0', port=7860)
config.json ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ {
2
+ "model": "qwen3.6-plus",
3
+ "timeout_seconds": 86400
4
+ }
outline.md ADDED
@@ -0,0 +1,248 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Outline — 多Agent协作物理解题系统
2
+
3
+ ## System: Orchestrator
4
+
5
+ 你是 Orchestrator(编排者),负责协调多个 sub-Agent 解决物理题目。
6
+
7
+ **工作方式:**
8
+ 1. 用 read_file 读取项目根目录的 outline.md,理解完整的工作流架构和各 Agent 定义
9
+ 2. 从 outline.md 的 `## Architecture` 部分了解执行顺序、数据流和反馈规则
10
+ 3. 从 outline.md 的 `## Agent: <name>` 部分提取对应 Agent 的 prompt
11
+ 4. **自动识别输入结构**:
12
+ - 如果指定目录下存在若干子文件夹,每个子文件夹内含 problem.md → 视为多题目录,依次串行处理每个子文件夹
13
+ - 否则 → 视为单题目录,读取该目录下 problem.md 作为唯一题目
14
+ - 多题场景下,每道题独立执行步骤 5-9,全部完成后在**父目录**生成 `batch_summary.md` 汇总所有子题目结果
15
+ 5. 对每一道题,按 Architecture 定义的顺序,用 Bash 调用 spawn.py 逐个创建 sub-Agent:
16
+ ```
17
+ python3 spawn.py <role> <workspace> <prompt_file> <task_file>
18
+ ```
19
+ - `<role>`: Agent 角色名(Planner / Builder / Evaluator)
20
+ - `<workspace>`: 工作目录路径
21
+ - `<prompt_file>`: 临时文件,先写入从 outline.md 提取的 Agent prompt
22
+ - `<task_file>`: 临时文件,先写入任务描述(要读什么文件、输出到什么文件)
23
+ - spawn.py 会创建一个 Claude Code 子进程,完成后将结果写入 `<workspace>/.<role>.result`
24
+ 6. 记录每个 sub-Agent 的调用轮次、用时和结果
25
+ 7. 全部阶段完成后,检查 Evaluator 的输出文件:
26
+ - 包含 "PASS" → 将解题结果按合理格式写入 {workspace}/final_summary.md,结束
27
+ - 包含 "REVISE" → 按 Architecture 反馈规则重新执行相关 Agent,最多迭代 2 次
28
+ 8. 迭代时,将审查意见作为额外上下文加入 Builder 的 task 描述
29
+ 9. 第二次迭代仍 REVISE → 将当前最佳方案和未解决问题列表写入 {workspace}/final_summary.md,结束
30
+
31
+ **输出格式:**
32
+ - **单题**:在 final_summary.md 中,请包含以下信息:
33
+ - 各阶段的执行统计:读每个 `.{role}.metrics` 文件(JSON),提取 duration_ms、usage 中的 tokens,汇总轮次、总用时、总 Token 消耗
34
+ - 最终答案的完整呈现
35
+ - 格式清晰、易读
36
+ - **多题**:在父目录生成 `batch_summary.md`,包含每道题的子目录名、是否 PASS、最终答案摘要、轮次和用时。
37
+
38
+ **原则:**
39
+ - 你自己不做具体的物理解题——所有分析、求解、审查都委托给 sub-Agent
40
+ - 你只负责编排:读配置、分配任务、传递上下文、判断是否迭代
41
+ - 每个 sub-Agent 是独立的 Claude Code 进程,完成后返回结果文本
42
+
43
+
44
+ ## Agent: Planner
45
+
46
+ 你是 Planner(规划者),负责分析物理题目并制定解题计划。
47
+
48
+ **输入:** 用 read_file 读取 task 中指定的文件(problem.md,可能还有前置输出)。
49
+ **输出:** 用 write_file 将解题计划写入 task 中指定的输出文件。
50
+
51
+ **解题计划应包含:**
52
+
53
+ ### 物理情景
54
+ - 用文字描述物理过程和示意图(标注物体、力/场的方向、坐标系)
55
+ - 明确物理过程的阶段划分(如:加速阶段、碰撞前后等)
56
+
57
+ ### 符号约定表
58
+ | 符号 | 含义 | 数值 | 单位 |
59
+ - 已知量和未知量分开列出
60
+
61
+ ### 适用物理定律
62
+ - 列出每条定律名称和对应公式
63
+ - 说明为什么适用(满足什么前提条件)
64
+
65
+ ### 逐步解题路线
66
+ - 从待求量出发逆向推导:需要什么中间量 → 用什么定律得到 → 写出对应方程
67
+ - 验证方程可解性:未知量个数 = 独立方程个数
68
+
69
+ ### 量纲预检
70
+ - 关键等式两边的量纲是否一致
71
+
72
+ ### 特殊情况
73
+ - 边界条件、极端情况、多解可能性
74
+
75
+
76
+ ## Agent: Builder
77
+
78
+ 你是 Builder(求解者),负责基于解题计划执行完整推导。
79
+
80
+ **输入:** 用 read_file 读取 task 中指定的文件(problem.md + plan.md)。
81
+ **输出:** 用 write_file 将求解过程写入 task 中指定的输出文件。
82
+ **可选技能:** 你可以在推导过程中运用 outline.md 中定义的 Skills(如 calculation、dimension_check),不需要调用外部工具——凭自身能力按 Skill 描述执行即可。
83
+
84
+ **求解过程应包含:**
85
+
86
+ ### 逐步推导
87
+ 每一步格式:
88
+ > **Step N: [定律名]**
89
+ > 公式:...
90
+ > 代入:... = ...
91
+ > 结果:... = ... (单位)
92
+
93
+ - 代数推导完整,不跳步
94
+ - 每个数值结果带单位和有效数字
95
+ - 关键步骤后穿插量纲检查
96
+
97
+ ### 最终答案
98
+ - 醒目标注,带完整单位
99
+ - 对应题目要求的每个小问逐一回答
100
+
101
+ ### 合理性检验
102
+ - 数量级是否合理(与常识对比)
103
+ - 方向是否正确
104
+ - 极端参数退化是否合理(如 μ→0, θ→0 等)
105
+
106
+ 如果 task 中包含审查反馈(REVISE 上下文),请先根据反馈定位问题,再修正推导。
107
+
108
+
109
+ ## Agent: Evaluator
110
+
111
+ 你是 Evaluator(审查者),负责严格审查物理求解过程。
112
+
113
+ **输入:** 用 read_file 读取 task 中指定的文件(problem.md + solution.md,可能还有 plan.md)。
114
+ **输出:** 用 write_file 将审查结果写入 task 中指定的输出文件。
115
+
116
+ **审查清单:**
117
+ 1. **题意覆盖** — 是否遗漏已知条件?是否误解题意?是否回答了所有小问?
118
+ 2. **模型合理性** — 坐标系/参考系选择是否合理?近似条件是否成立?
119
+ 3. **推导正确性** — 每步定律适用是否正确?公式是否有误?代数运算是否正确?
120
+ 4. **量纲一致性** — 每步等式两边量纲?最终答案单位?
121
+ 5. **数值合理性** — 有效数字?数量级可信?
122
+ 6. **合理性检验** — 是否做了充分的自洽检查?
123
+
124
+ **输出格式(严格遵守):**
125
+ - 解答正确完整 → 第一行写 `PASS`,后面附简要肯定说明
126
+ - 存在问题 → 第一行写 `REVISE`,后面逐条列出:
127
+ - 在哪一步
128
+ - 错在何处
129
+ - 应如何修正
130
+ 描述要足够具体,让 Builder 能直接定位并修正
131
+
132
+
133
+ ## Architecture
134
+
135
+ ### 执行顺序
136
+ ```
137
+ Orchestrator
138
+ │
139
+ ├─→ spawn Planner
140
+ │ 读 problem.md → 写 {workspace}/plan.md
141
+ │
142
+ ├─→ spawn Builder
143
+ │ 读 problem.md + {workspace}/plan.md → 写 {workspace}/solution.md
144
+ │
145
+ ├─→ spawn Evaluator
146
+ │ 读 problem.md + {workspace}/solution.md → 写 {workspace}/review.md
147
+ │
148
+ └─→ 检查 review.md
149
+ PASS → 写 {workspace}/final_summary.md,结束
150
+ REVISE → 迭代(见下方反馈规则)
151
+ ```
152
+
153
+ ### 反馈规则
154
+ - 若 review.md 第一行为 `REVISE`:
155
+ 1. Orchestrator 重新 spawn Builder,task 中附带审查意见原文
156
+ 2. Builder 修正后写 {workspace}/solution.md
157
+ 3. Orchestrator spawn Evaluator 重新审查
158
+ 4. 最多迭代 **2** 次
159
+ - 第二次迭代仍 REVISE → 将当前最佳方案和未解决问题列表写入 {workspace}/final_summary.md,结束
160
+
161
+
162
+ ## Format
163
+
164
+ 各 Agent 输出文件使用 Markdown 格式,遵循以下结构。
165
+
166
+ ### plan.md
167
+ ```markdown
168
+ # 解题计划 — [题目简述]
169
+
170
+ ## 物理情景
171
+ [文字描述 + 示意图]
172
+
173
+ ## 符号约定
174
+ | 符号 | 含义 | 数值 | 单位 |
175
+ |------|------|------|------|
176
+
177
+ ## 适用定律
178
+ 1. [定律名]: [公式] — 适用原因: ...
179
+
180
+ ## 解题路线
181
+ Step 1: [定律] → [方程] → [求出量]
182
+ Step 2: ...
183
+
184
+ ## 可解性检查
185
+ 未知量 N 个 = 独立方程 N 个 ✓/✗
186
+
187
+ ## 量纲预检
188
+ [关键等式的量纲验证]
189
+
190
+ ## 特殊情况
191
+ [边界/极端/多解]
192
+ ```
193
+
194
+ ### solution.md
195
+ ```markdown
196
+ # 求解过程
197
+
198
+ **Step 1: [定律名]**
199
+ 公式: ...
200
+ 代入: ...
201
+ 结果: ... = ... (单位)
202
+
203
+ ...
204
+
205
+ ## 最终答案
206
+ 1. [小问1]: ... = ... (单位)
207
+ 2. [小问2]: ...
208
+
209
+ ## 合理性检验
210
+ [数量级/方向/极端情况验证]
211
+ ```
212
+
213
+ ### review.md
214
+ ```markdown
215
+ PASS
216
+ [简要肯定说明]
217
+
218
+ 或
219
+
220
+ REVISE
221
+ 1. [步骤]: [问题描述] → [修正建议]
222
+ 2. ...
223
+ ```
224
+
225
+ ### 文件命名约定
226
+ - 输入文件统一为 `{workspace}/problem.md`
227
+ - 各阶段输出文件名固定: `plan.md`, `solution.md`, `review.md`
228
+ - workspace 目录路径由 Orchestrator 在 task 中指定
229
+
230
+
231
+ ## Skills
232
+
233
+ Skills 是 Agent 可调用的问题解决能力,由模型自身执行(无外部脚本)。
234
+ Orchestrator 在 spawn Agent 时会将 Agent prompt 中引用的 Skill 内容一并传入。
235
+ 新增 Skill 只需在本节添加定义,并在对应 Agent prompt 中声明引用。
236
+
237
+ ### Skill: calculation
238
+ 数值计算与代数推导验证。
239
+ - 对关键数值结果做手算复核:列出完整的数值代入过程,逐步计算
240
+ - 对代数推导做交叉验证:用不同路线(如能量法 vs 力学法)验证同一结论
241
+ - 结果标注有效数字,与粗略估计对比数量级
242
+
243
+ ### Skill: dimension_check
244
+ 量纲一致性检查。
245
+ - 逐步验证每个物理等式:左右两边量纲是否相同
246
+ - 验证最终答案的量纲是否与待求量的物理意义匹配
247
+ - 常见SI量纲:m(L), kg(M), s(T), N(M·L·T⁻²), J(M·L²·T⁻²), m/s(L·T⁻¹), m/s²(L·T⁻²)
248
+ - 检查时将所有量展开为基本量纲(L,M,T)后对比
requirements.txt ADDED
@@ -0,0 +1 @@
 
 
1
+ Flask==2.3.3
run.py ADDED
@@ -0,0 +1,79 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Minimal bootstrap — launches Orchestrator Agent via Claude Code CLI."""
3
+
4
+ import sys, os, json, re, subprocess
5
+
6
+ ROOT = os.path.dirname(os.path.abspath(__file__))
7
+ os.chdir(ROOT)
8
+
9
+ CONFIG = json.loads(open(os.path.join(ROOT, "config.json"), encoding="utf-8").read())
10
+ MODEL = CONFIG.get("model", "sonnet")
11
+ TIMEOUT = CONFIG.get("timeout_seconds", 600)
12
+
13
+ SUMMARY_FILE = "problems/001/final_summary.md"
14
+
15
+
16
+ def extract_section(md, heading):
17
+ """Extract content after '## <heading>' until the next ## or EOF."""
18
+ pattern = rf"^##\s+{re.escape(heading)}\s*\n(.*?)(?=^##\s|\Z)"
19
+ m = re.search(pattern, md, re.MULTILINE | re.DOTALL)
20
+ return m.group(1).strip() if m else ""
21
+
22
+
23
+ def main():
24
+ workspace = sys.argv[1] if len(sys.argv) > 1 else "problems/001"
25
+
26
+ outline = open(os.path.join(ROOT, "outline.md"), encoding="utf-8").read()
27
+ orchestrator_prompt = extract_section(outline, "System: Orchestrator")
28
+
29
+ if not orchestrator_prompt:
30
+ print("Error: cannot find '## System: Orchestrator' in outline.md")
31
+ sys.exit(1)
32
+
33
+ agents_json = json.dumps({
34
+ "Orchestrator": {
35
+ "description": "Orchestrator — 编排多个Agent解决物理题目",
36
+ "prompt": orchestrator_prompt,
37
+ }
38
+ })
39
+
40
+ cmd = [
41
+ "claude",
42
+ "--print",
43
+ "--output-format", "json",
44
+ "--permission-mode", "bypassPermissions",
45
+ "--bare",
46
+ "--agents", agents_json,
47
+ "--agent", "Orchestrator",
48
+ "--allowed-tools", "Bash,Read,Write",
49
+ "--add-dir", workspace,
50
+ "--model", MODEL,
51
+ f"请解决 {workspace}/problem.md 中的物理题目。\n"
52
+ f"先读取 outline.md 了解工作流架构和各 Agent 定义,然后按照架构执行。\n"
53
+ f"创建子 Agent 的方法:Bash 调用 spawn.py <role> <workspace> <prompt_file> <task_file>\n"
54
+ f"全部阶段完成后,将最终解题结果(包含轮次统计、用时、Token消耗等关键指标和解题内容)写入 {workspace}/final_summary.md 文件。\n"
55
+ f"工作目录: {workspace}",
56
+ ]
57
+
58
+ result = subprocess.run(cmd, capture_output=True, text=True, timeout=TIMEOUT)
59
+
60
+ try:
61
+ data = json.loads(result.stdout)
62
+ except json.JSONDecodeError:
63
+ print(f"Error: JSON parse failed\n{result.stdout[-500:]}")
64
+ sys.exit(1)
65
+
66
+ if data.get("is_error"):
67
+ print(f"Error: {data.get('result', 'Unknown')[:300]}")
68
+ sys.exit(1)
69
+
70
+ # Print whatever Orchestrator wrote
71
+ summary_path = os.path.join(workspace, "final_summary.md")
72
+ if os.path.exists(summary_path):
73
+ print(open(summary_path, encoding="utf-8").read())
74
+ else:
75
+ print("No summary file found.")
76
+
77
+
78
+ if __name__ == "__main__":
79
+ main()
settings.json ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ {
2
+ "apiKeyHelper": "echo $ANTHROPIC_API_KEY"
3
+ }
solver.py ADDED
@@ -0,0 +1,194 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Background task manager for the physics solver."""
3
+
4
+ import os
5
+ import sys
6
+ import json
7
+ import time
8
+ import shutil
9
+ import subprocess
10
+ import threading
11
+ from datetime import datetime, timezone
12
+
13
+ # Solver source location: Docker uses /app/solver, local falls back to project root
14
+ SOLVER_SRC = "/app/solver" if os.path.isdir("/app/solver") else os.path.dirname(os.path.abspath(__file__))
15
+ WORKSPACE_BASE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "workspaces")
16
+ DEFAULT_TIMEOUT = int(os.environ.get("SOLVER_TIMEOUT", "600"))
17
+ DEFAULT_MODEL = os.environ.get("SOLVER_MODEL", "qwen3.6-plus")
18
+ MAX_TASKS = int(os.environ.get("SOLVER_MAX_TASKS", "1"))
19
+
20
+ tasks = {}
21
+ tasks_lock = threading.Lock()
22
+
23
+
24
+ def _now():
25
+ return datetime.now(timezone.utc).isoformat()
26
+
27
+
28
+ def active_count():
29
+ with tasks_lock:
30
+ return sum(1 for t in tasks.values() if t["status"] in ("pending", "running"))
31
+
32
+
33
+ def submit_task(problem_text):
34
+ """Create a new task, return task_id or None if at capacity."""
35
+ with tasks_lock:
36
+ if active_count() >= MAX_TASKS:
37
+ return None
38
+
39
+ task_id = f"task-{int(time.time()*1000)}"
40
+ workspace = os.path.join(WORKSPACE_BASE, task_id)
41
+ os.makedirs(workspace, exist_ok=True)
42
+
43
+ # Write problem
44
+ with open(os.path.join(workspace, "problem.md"), "w", encoding="utf-8") as f:
45
+ f.write(problem_text)
46
+
47
+ # Copy solver source into workspace
48
+ for name in ("run.py", "spawn.py", "outline.md", "config.json"):
49
+ src = os.path.join(SOLVER_SRC, name)
50
+ if os.path.exists(src):
51
+ shutil.copy2(src, os.path.join(workspace, name))
52
+
53
+ # Override config.json with env-driven values
54
+ config_path = os.path.join(workspace, "config.json")
55
+ config = {"model": DEFAULT_MODEL, "timeout_seconds": DEFAULT_TIMEOUT}
56
+ with open(config_path, "w", encoding="utf-8") as f:
57
+ json.dump(config, f)
58
+
59
+ task = {
60
+ "task_id": task_id,
61
+ "status": "pending",
62
+ "phase": "waiting",
63
+ "workspace": workspace,
64
+ "problem": problem_text[:200],
65
+ "created_at": _now(),
66
+ "started_at": None,
67
+ "finished_at": None,
68
+ "error": None,
69
+ }
70
+ with tasks_lock:
71
+ tasks[task_id] = task
72
+
73
+ # Spawn background thread
74
+ t = threading.Thread(target=_run_solver, args=(task_id, workspace), daemon=True)
75
+ t.start()
76
+ return task_id
77
+
78
+
79
+ def _run_solver(task_id, workspace):
80
+ with tasks_lock:
81
+ tasks[task_id].update({"status": "running", "started_at": _now()})
82
+
83
+ env = os.environ.copy()
84
+ start = time.time()
85
+
86
+ try:
87
+ proc = subprocess.Popen(
88
+ [sys.executable, os.path.join(workspace, "run.py"), workspace],
89
+ stdout=subprocess.PIPE,
90
+ stderr=subprocess.PIPE,
91
+ cwd=workspace,
92
+ env=env,
93
+ )
94
+
95
+ # Monitor workspace for progress
96
+ while proc.poll() is None:
97
+ elapsed = time.time() - start
98
+ if elapsed > DEFAULT_TIMEOUT:
99
+ proc.kill()
100
+ with tasks_lock:
101
+ tasks[task_id].update({
102
+ "status": "error",
103
+ "error": f"Timeout after {elapsed:.0f}s",
104
+ "finished_at": _now(),
105
+ })
106
+ return
107
+
108
+ # Infer phase from workspace files
109
+ phase = _infer_phase(workspace)
110
+ with tasks_lock:
111
+ tasks[task_id]["phase"] = phase
112
+
113
+ time.sleep(2)
114
+
115
+ stdout, stderr = proc.communicate()
116
+
117
+ if proc.returncode != 0:
118
+ with tasks_lock:
119
+ tasks[task_id].update({
120
+ "status": "error",
121
+ "error": stderr.decode("utf-8", errors="replace")[-500:],
122
+ "finished_at": _now(),
123
+ })
124
+ return
125
+
126
+ # Check for final summary
127
+ summary = os.path.join(workspace, "final_summary.md")
128
+ if os.path.exists(summary):
129
+ with tasks_lock:
130
+ tasks[task_id].update({
131
+ "status": "complete",
132
+ "phase": "complete",
133
+ "finished_at": _now(),
134
+ })
135
+ else:
136
+ with tasks_lock:
137
+ tasks[task_id].update({
138
+ "status": "error",
139
+ "error": "Solver finished but no summary file was produced.",
140
+ "finished_at": _now(),
141
+ })
142
+
143
+ except Exception as e:
144
+ with tasks_lock:
145
+ tasks[task_id].update({
146
+ "status": "error",
147
+ "error": str(e),
148
+ "finished_at": _now(),
149
+ })
150
+
151
+
152
+ def _infer_phase(workspace):
153
+ """Check workspace files to determine current solver phase."""
154
+ if os.path.exists(os.path.join(workspace, "final_summary.md")):
155
+ return "complete"
156
+ if os.path.exists(os.path.join(workspace, "review.md")):
157
+ return "evaluating"
158
+ if os.path.exists(os.path.join(workspace, "solution.md")):
159
+ return "building"
160
+ if os.path.exists(os.path.join(workspace, "plan.md")):
161
+ return "planning"
162
+ return "starting"
163
+
164
+
165
+ def get_task(task_id):
166
+ with tasks_lock:
167
+ return dict(tasks.get(task_id, {}))
168
+
169
+
170
+ def get_task_result(task_id):
171
+ """Read final_summary.md if available."""
172
+ task = get_task(task_id)
173
+ if not task:
174
+ return None
175
+ summary = os.path.join(task.get("workspace", ""), "final_summary.md")
176
+ if os.path.exists(summary):
177
+ with open(summary, encoding="utf-8") as f:
178
+ return f.read()
179
+ return None
180
+
181
+
182
+ def list_tasks():
183
+ with tasks_lock:
184
+ return [
185
+ {
186
+ "task_id": t["task_id"],
187
+ "status": t["status"],
188
+ "phase": t["phase"],
189
+ "problem": t["problem"],
190
+ "created_at": t["created_at"],
191
+ "finished_at": t.get("finished_at"),
192
+ }
193
+ for t in sorted(tasks.values(), key=lambda x: x["created_at"], reverse=True)
194
+ ]
spawn.py ADDED
@@ -0,0 +1,85 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Helper script — spawn a sub-Agent via Claude Code CLI.
3
+ Called by the Orchestrator through Bash tool use.
4
+
5
+ Usage: spawn.py <role> <workspace> <prompt_file> <task_file> [--tools Read,Write]
6
+ """
7
+
8
+ import sys, os, json, subprocess
9
+
10
+ ROOT = os.path.dirname(os.path.abspath(__file__))
11
+ CONFIG = json.loads(open(os.path.join(ROOT, "config.json"), encoding="utf-8").read())
12
+ MODEL = CONFIG.get("model", "sonnet")
13
+ TIMEOUT = CONFIG.get("timeout_seconds", 600)
14
+
15
+
16
+ def main():
17
+ if len(sys.argv) < 5:
18
+ print("Usage: spawn.py <role> <workspace> <prompt_file> <task_file> [--tools Read,Write]")
19
+ sys.exit(1)
20
+
21
+ role = sys.argv[1]
22
+ workspace = sys.argv[2]
23
+ prompt_file = sys.argv[3]
24
+ task_file = sys.argv[4]
25
+
26
+ # Parse optional --tools
27
+ allowed_tools = "Read,Write"
28
+ for i, arg in enumerate(sys.argv):
29
+ if arg == "--tools" and i + 1 < len(sys.argv):
30
+ allowed_tools = sys.argv[i + 1]
31
+
32
+ system_prompt = open(prompt_file, encoding="utf-8").read()
33
+ task = open(task_file, encoding="utf-8").read()
34
+ if workspace:
35
+ task += f"\n工作目录: {workspace}"
36
+
37
+ agents_json = json.dumps({role: {"description": f"{role} Agent", "prompt": system_prompt}})
38
+
39
+ cmd = [
40
+ "claude",
41
+ "--print",
42
+ "--output-format", "json",
43
+ "--permission-mode", "bypassPermissions",
44
+ "--bare",
45
+ "--agents", agents_json,
46
+ "--agent", role,
47
+ "--allowed-tools", allowed_tools,
48
+ "--add-dir", workspace,
49
+ "--model", MODEL,
50
+ task,
51
+ ]
52
+
53
+ result = subprocess.run(cmd, capture_output=True, text=True, timeout=TIMEOUT)
54
+
55
+ try:
56
+ data = json.loads(result.stdout)
57
+ except json.JSONDecodeError:
58
+ print(f"[spawn:{role}] error: JSON parse failed — {result.stdout[-300:]}")
59
+ sys.exit(1)
60
+
61
+ if data.get("is_error"):
62
+ print(f"[spawn:{role}] error: {data.get('result', 'Unknown')[:300]}")
63
+ sys.exit(1)
64
+
65
+ # Write result text for Orchestrator
66
+ result_path = os.path.join(workspace, f".{role}.result")
67
+ with open(result_path, "w", encoding="utf-8") as f:
68
+ f.write(data.get("result", ""))
69
+
70
+ # Write metrics for Orchestrator to collect
71
+ metrics = {
72
+ "role": role,
73
+ "duration_ms": data.get("duration_ms", 0),
74
+ "duration_api_ms": data.get("duration_api_ms", 0),
75
+ "usage": data.get("usage", {}),
76
+ }
77
+ metrics_path = os.path.join(workspace, f".{role}.metrics")
78
+ with open(metrics_path, "w", encoding="utf-8") as f:
79
+ json.dump(metrics, f, ensure_ascii=False)
80
+
81
+ print(f"[spawn:{role}] done → {result_path}")
82
+
83
+
84
+ if __name__ == "__main__":
85
+ main()
static/solver.html ADDED
@@ -0,0 +1,289 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Physics Problem Solver</title>
7
+ <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
8
+ <style>
9
+ * { box-sizing: border-box; margin: 0; padding: 0; }
10
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #f0f2f5; color: #333; min-height: 100vh; }
11
+
12
+ .container { max-width: 960px; margin: 0 auto; padding: 20px; }
13
+ h1 { text-align: center; padding: 20px 0 10px; color: #1a1a2e; font-size: 1.8em; }
14
+ .subtitle { text-align: center; color: #666; margin-bottom: 24px; font-size: 0.95em; }
15
+
16
+ .card { background: #fff; border-radius: 12px; box-shadow: 0 2px 8px rgba(0,0,0,0.08); padding: 24px; margin-bottom: 20px; }
17
+
18
+ textarea { width: 100%; min-height: 180px; border: 1px solid #ddd; border-radius: 8px; padding: 12px; font-size: 14px; font-family: inherit; resize: vertical; line-height: 1.6; }
19
+ textarea:focus { outline: none; border-color: #4361ee; box-shadow: 0 0 0 3px rgba(67,97,238,0.15); }
20
+
21
+ .btn-row { display: flex; gap: 12px; margin-top: 12px; }
22
+ .btn { padding: 10px 28px; border: none; border-radius: 8px; font-size: 15px; cursor: pointer; font-weight: 600; transition: all 0.15s; }
23
+ .btn-primary { background: #4361ee; color: #fff; }
24
+ .btn-primary:hover:not(:disabled) { background: #3a56d4; }
25
+ .btn-primary:disabled { background: #a0b0f0; cursor: not-allowed; }
26
+ .btn-secondary { background: #e8e8e8; color: #333; }
27
+ .btn-secondary:hover { background: #d8d8d8; }
28
+
29
+ /* Progress Steps */
30
+ .progress-bar { display: none; margin: 16px 0 8px; }
31
+ .progress-bar.visible { display: block; }
32
+ .steps { display: flex; justify-content: space-between; position: relative; margin-bottom: 8px; }
33
+ .steps::before { content: ''; position: absolute; top: 14px; left: 40px; right: 40px; height: 3px; background: #e0e0e0; z-index: 0; }
34
+ .step { position: relative; z-index: 1; text-align: center; flex: 1; }
35
+ .step-circle { width: 28px; height: 28px; border-radius: 50%; background: #e0e0e0; margin: 0 auto 6px; display: flex; align-items: center; justify-content: center; font-size: 13px; color: #999; transition: all 0.3s; }
36
+ .step.active .step-circle { background: #4361ee; color: #fff; }
37
+ .step.done .step-circle { background: #2ecc71; color: #fff; }
38
+ .step-label { font-size: 12px; color: #888; }
39
+ .step.active .step-label { color: #4361ee; font-weight: 600; }
40
+ .step.done .step-label { color: #2ecc71; }
41
+
42
+ .elapsed { text-align: center; color: #888; font-size: 13px; margin-top: 4px; }
43
+
44
+ /* Result */
45
+ .result { display: none; margin-top: 16px; }
46
+ .result.visible { display: block; }
47
+ .result h2 { margin: 12px 0 8px; font-size: 1.2em; color: #1a1a2e; }
48
+ .result-content { line-height: 1.7; }
49
+ .result-content table { border-collapse: collapse; width: 100%; margin: 12px 0; }
50
+ .result-content th, .result-content td { border: 1px solid #ddd; padding: 8px 12px; text-align: left; }
51
+ .result-content th { background: #f5f5f5; }
52
+ .result-content pre { background: #f8f8f8; padding: 12px; border-radius: 6px; overflow-x: auto; font-size: 13px; }
53
+ .result-content code { background: #f0f0f0; padding: 1px 4px; border-radius: 3px; font-size: 13px; }
54
+ .result-content pre code { background: none; padding: 0; }
55
+ .result-content blockquote { border-left: 3px solid #4361ee; padding-left: 12px; color: #555; margin: 8px 0; }
56
+
57
+ /* Task list */
58
+ .task-list { max-height: 200px; overflow-y: auto; }
59
+ .task-item { display: flex; justify-content: space-between; align-items: center; padding: 8px 0; border-bottom: 1px solid #f0f0f0; cursor: pointer; }
60
+ .task-item:last-child { border-bottom: none; }
61
+ .task-item:hover { background: #fafafa; margin: 0 -24px; padding: 8px 24px; border-radius: 4px; }
62
+ .task-id { font-family: monospace; font-size: 13px; color: #666; }
63
+ .badge { font-size: 11px; padding: 2px 8px; border-radius: 10px; font-weight: 600; }
64
+ .badge-pending { background: #fff3cd; color: #856404; }
65
+ .badge-running { background: #cce5ff; color: #004085; }
66
+ .badge-complete { background: #d4edda; color: #155724; }
67
+ .badge-error { background: #f8d7da; color: #721c24; }
68
+
69
+ .error-msg { background: #f8d7da; color: #721c24; padding: 12px; border-radius: 8px; margin-top: 12px; display: none; }
70
+ .error-msg.visible { display: block; }
71
+ </style>
72
+ </head>
73
+ <body>
74
+ <div class="container">
75
+ <h1>Physics Problem Solver</h1>
76
+ <p class="subtitle">Multi-agent AI system: Plan &rarr; Build &rarr; Evaluate</p>
77
+
78
+ <div class="card">
79
+ <label style="font-weight:600;margin-bottom:8px;display:block;">Input Physics Problem</label>
80
+ <textarea id="problem-input" placeholder="e.g. A 2.0 kg object slides down a 30-degree rough incline..."></textarea>
81
+ <div class="btn-row">
82
+ <button class="btn btn-primary" id="submit-btn" onclick="submitProblem()">Solve</button>
83
+ </div>
84
+
85
+ <!-- Progress -->
86
+ <div class="progress-bar" id="progress-bar">
87
+ <div class="steps">
88
+ <div class="step" id="step-starting"><div class="step-circle">1</div><div class="step-label">Starting</div></div>
89
+ <div class="step" id="step-planning"><div class="step-circle">2</div><div class="step-label">Planning</div></div>
90
+ <div class="step" id="step-building"><div class="step-circle">3</div><div class="step-label">Building</div></div>
91
+ <div class="step" id="step-evaluating"><div class="step-circle">4</div><div class="step-label">Evaluating</div></div>
92
+ <div class="step" id="step-complete"><div class="step-circle">5</div><div class="step-label">Complete</div></div>
93
+ </div>
94
+ <div class="elapsed" id="elapsed"></div>
95
+ </div>
96
+
97
+ <div class="error-msg" id="error-msg"></div>
98
+
99
+ <!-- Result -->
100
+ <div class="result" id="result">
101
+ <div class="result-content" id="result-content"></div>
102
+ <div class="btn-row">
103
+ <button class="btn btn-secondary" onclick="resetForm()">New Problem</button>
104
+ </div>
105
+ </div>
106
+ </div>
107
+
108
+ <!-- Task History -->
109
+ <div class="card">
110
+ <h2 style="font-size:1em;margin-bottom:12px;">Recent Tasks</h2>
111
+ <div class="task-list" id="task-list">
112
+ <p style="color:#999;font-size:13px;">No tasks yet.</p>
113
+ </div>
114
+ </div>
115
+ </div>
116
+
117
+ <script>
118
+ let currentTaskId = null;
119
+ let pollTimer = null;
120
+ let startTime = null;
121
+ let elapsedTimer = null;
122
+
123
+ const phaseOrder = ['starting', 'planning', 'building', 'evaluating', 'complete'];
124
+
125
+ function setStatus(status) {
126
+ document.getElementById('submit-btn').disabled = (status === 'running');
127
+ }
128
+
129
+ function updateProgress(phase) {
130
+ phaseOrder.forEach(p => {
131
+ const el = document.getElementById('step-' + p);
132
+ el.classList.remove('active', 'done');
133
+ if (p === phase) el.classList.add('active');
134
+ else if (phaseOrder.indexOf(p) < phaseOrder.indexOf(phase)) el.classList.add('done');
135
+ });
136
+ }
137
+
138
+ function showError(msg) {
139
+ const el = document.getElementById('error-msg');
140
+ el.textContent = msg;
141
+ el.classList.add('visible');
142
+ }
143
+
144
+ function hideError() {
145
+ document.getElementById('error-msg').classList.remove('visible');
146
+ }
147
+
148
+ async function submitProblem() {
149
+ const text = document.getElementById('problem-input').value.trim();
150
+ if (!text) return;
151
+
152
+ hideError();
153
+ document.getElementById('result').classList.remove('visible');
154
+ document.getElementById('progress-bar').classList.add('visible');
155
+ setStatus('running');
156
+
157
+ try {
158
+ const resp = await fetch('/api/submit', {
159
+ method: 'POST',
160
+ headers: {'Content-Type': 'application/json'},
161
+ body: JSON.stringify({problem: text})
162
+ });
163
+ const data = await resp.json();
164
+ if (data.error) { showError(data.error); setStatus('idle'); return; }
165
+
166
+ currentTaskId = data.task_id;
167
+ startTime = Date.now();
168
+ startElapsed();
169
+ pollStatus();
170
+ } catch (e) {
171
+ showError('Network error: ' + e.message);
172
+ setStatus('idle');
173
+ }
174
+ }
175
+
176
+ function pollStatus() {
177
+ if (pollTimer) clearInterval(pollTimer);
178
+ pollTimer = setInterval(async () => {
179
+ if (!currentTaskId) return;
180
+ try {
181
+ const resp = await fetch('/api/status/' + currentTaskId);
182
+ const data = await resp.json();
183
+ if (data.error) { showError(data.error); stopPolling(); return; }
184
+
185
+ updateProgress(data.phase || 'starting');
186
+
187
+ if (data.status === 'complete') {
188
+ stopPolling();
189
+ await loadResult();
190
+ } else if (data.status === 'error') {
191
+ stopPolling();
192
+ showError(data.error || 'Task failed');
193
+ setStatus('idle');
194
+ }
195
+ } catch (e) { /* retry next poll */ }
196
+ }, 3000);
197
+ }
198
+
199
+ function stopPolling() {
200
+ if (pollTimer) { clearInterval(pollTimer); pollTimer = null; }
201
+ if (elapsedTimer) { clearInterval(elapsedTimer); elapsedTimer = null; }
202
+ }
203
+
204
+ function startElapsed() {
205
+ if (elapsedTimer) clearInterval(elapsedTimer);
206
+ elapsedTimer = setInterval(() => {
207
+ if (!startTime) return;
208
+ const s = Math.floor((Date.now() - startTime) / 1000);
209
+ const m = Math.floor(s / 60);
210
+ document.getElementById('elapsed').textContent = `Elapsed: ${m}m ${s % 60}s`;
211
+ }, 1000);
212
+ }
213
+
214
+ async function loadResult() {
215
+ try {
216
+ const resp = await fetch('/api/result/' + currentTaskId);
217
+ const data = await resp.json();
218
+ if (data.content) {
219
+ document.getElementById('result-content').innerHTML = marked.parse(data.content);
220
+ document.getElementById('result').classList.add('visible');
221
+ }
222
+ setStatus('idle');
223
+ } catch (e) {
224
+ showError('Failed to load result');
225
+ setStatus('idle');
226
+ }
227
+ }
228
+
229
+ function resetForm() {
230
+ currentTaskId = null;
231
+ startTime = null;
232
+ stopPolling();
233
+ document.getElementById('problem-input').value = '';
234
+ document.getElementById('progress-bar').classList.remove('visible');
235
+ document.getElementById('result').classList.remove('visible');
236
+ hideError();
237
+ setStatus('idle');
238
+ phaseOrder.forEach(p => {
239
+ document.getElementById('step-' + p).classList.remove('active', 'done');
240
+ });
241
+ }
242
+
243
+ async function loadTaskList() {
244
+ try {
245
+ const resp = await fetch('/api/tasks');
246
+ const data = await resp.json();
247
+ const el = document.getElementById('task-list');
248
+ if (!data.tasks || data.tasks.length === 0) {
249
+ el.innerHTML = '<p style="color:#999;font-size:13px;">No tasks yet.</p>';
250
+ return;
251
+ }
252
+ el.innerHTML = data.tasks.slice(0, 20).map(t => {
253
+ const badgeClass = 'badge-' + t.status;
254
+ const time = t.created_at ? new Date(t.created_at).toLocaleTimeString() : '';
255
+ return `<div class="task-item" onclick="viewTask('${t.task_id}')">
256
+ <span class="task-id">${t.task_id}</span>
257
+ <span>${time}</span>
258
+ <span class="badge ${badgeClass}">${t.status}</span>
259
+ </div>`;
260
+ }).join('');
261
+ } catch (e) {}
262
+ }
263
+
264
+ async function viewTask(taskId) {
265
+ const resp = await fetch('/api/status/' + taskId);
266
+ const data = await resp.json();
267
+ if (data.status === 'complete') {
268
+ currentTaskId = taskId;
269
+ startTime = Date.now();
270
+ document.getElementById('progress-bar').classList.add('visible');
271
+ updateProgress(data.phase || 'complete');
272
+ await loadResult();
273
+ } else if (data.status === 'error') {
274
+ showError(data.error || 'Task failed');
275
+ } else {
276
+ currentTaskId = taskId;
277
+ startTime = Date.now();
278
+ document.getElementById('progress-bar').classList.add('visible');
279
+ updateProgress(data.phase || 'starting');
280
+ startElapsed();
281
+ pollStatus();
282
+ }
283
+ }
284
+
285
+ loadTaskList();
286
+ setInterval(loadTaskList, 5000);
287
+ </script>
288
+ </body>
289
+ </html>