specmodule 0.1.0__py3-none-any.whl

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.
@@ -0,0 +1,268 @@
1
+ # module_harness/submodule.py
2
+ """SubModule — 类式 submodule 定义 + 嵌入/完整运行 + pack 导出。"""
3
+
4
+ from __future__ import annotations
5
+
6
+ import inspect
7
+ import json
8
+ import textwrap
9
+ import uuid
10
+ from dataclasses import asdict
11
+ from pathlib import Path
12
+ from typing import Any, Callable, Literal
13
+
14
+ from llm import LLMConfig, create_llm_client
15
+
16
+ from .builtins import register_builtin_harnesses
17
+ from .command import CommandConfig
18
+ from .config import HarnessConfig
19
+ from .events import EventBus
20
+ from .module import Module
21
+ from .registry import HarnessRegistry
22
+ from .spec import SpecSchema, SpecValidationError, Tasklist
23
+
24
+
25
+ def script(name: str):
26
+ """类内 script 标记装饰器:标记函数,__init_subclass__ 时收集。
27
+
28
+ 脚本是类体内普通函数(不绑定 self),与 @reg.script 语义一致。
29
+ 注册名必须与函数名一致(函数名 = 注册名 = 打包文件名)。
30
+ """
31
+
32
+ def deco(fn: Callable) -> Callable:
33
+ if name != fn.__name__:
34
+ raise ValueError(
35
+ f"script 注册名 '{name}' 与函数名 '{fn.__name__}' 不一致"
36
+ )
37
+ fn._submodule_script_name = name # type: ignore[attr-defined]
38
+ return fn
39
+
40
+ return deco
41
+
42
+
43
+ class SubModule:
44
+ """类式 submodule 定义。类属性 = 注册信息,@script 收集脚本。
45
+
46
+ run() 内部组合 Module:注册 provides → 构造 Module → 运行。
47
+ pack() 导出发布目录(module.json + harnesses/ + scripts/ + commands/)。
48
+ """
49
+
50
+ name: str = ""
51
+ version: str = "0.1.0"
52
+ description: str = ""
53
+ spec_schema: SpecSchema = SpecSchema()
54
+ harnesses: list[HarnessConfig] = []
55
+ commands: list[CommandConfig] = []
56
+ requires: list[str] = []
57
+ guards: list[tuple[str, Callable]] = [] # [(名字, 函数)],名字 = 注册名 = 打包文件名
58
+ modules: dict[str, type["SubModule"]] = {} # submodule 节点引用表 {tasklist 名: 类}
59
+ tasklist: Tasklist | None = None
60
+ mode: Literal["persist", "fast"] = "persist"
61
+ # 发布者声明轻量特性:"fast" = 快速模式(NullBackend 全内存,零落盘零 I/O,
62
+ # D11);默认 "persist" 落盘到 .specmodule/runs/<run_id>/(D9)。
63
+ _scripts: dict[str, Callable] = {}
64
+
65
+ def __init_subclass__(cls, **kwargs: Any) -> None:
66
+ super().__init_subclass__(**kwargs)
67
+ inherited = dict(getattr(cls, "_scripts", {}))
68
+ collected = {
69
+ n: fn for n, fn in cls.__dict__.items()
70
+ if callable(fn) and getattr(fn, "_submodule_script_name", None)
71
+ }
72
+ cls._scripts = inherited
73
+ cls._scripts.update(collected)
74
+ # 列表类属性按子类复制,防止子类就地修改污染父类注册
75
+ for attr in ("harnesses", "commands", "requires", "guards"):
76
+ if attr not in cls.__dict__:
77
+ setattr(cls, attr, list(getattr(cls, attr)))
78
+ # dict 类属性同理由:按子类复制,防止子类就地修改污染父类注册
79
+ if "modules" not in cls.__dict__:
80
+ setattr(cls, "modules", dict(getattr(cls, "modules")))
81
+
82
+ def __init__(
83
+ self,
84
+ llm_client: Any = None,
85
+ event_bus: EventBus | None = None,
86
+ ) -> None:
87
+ self._llm_client = llm_client
88
+ self._event_bus = event_bus
89
+
90
+ def _ensure_client(self) -> Any:
91
+ """直接类使用(未注入 client)时从 env 懒创建。"""
92
+ if self._llm_client is None:
93
+ self._llm_client = create_llm_client(LLMConfig.from_env())
94
+ return self._llm_client
95
+
96
+ def _module_id(self) -> str:
97
+ return f"{self.name}_{uuid.uuid4().hex[:6]}"
98
+
99
+ def _build_registry(
100
+ self,
101
+ audit: bool,
102
+ harness_overrides: dict[str, Any] | None = None,
103
+ *,
104
+ llm_client: Any = None,
105
+ event_bus: EventBus | None = None,
106
+ ) -> HarnessRegistry:
107
+ # 事件投递与 keep_records/persist 解耦:宿主传了 event_bus 就始终投递
108
+ # (与 audit 无关);未传则静默 EventBus.null()(嵌入零开销)。audit 只
109
+ # 在 run() 里映射 keep_records。
110
+ bus = event_bus if event_bus is not None else (self._event_bus or EventBus.null())
111
+ client = llm_client if llm_client is not None else self._ensure_client()
112
+ reg = HarnessRegistry(llm_client=client, event_bus=bus)
113
+ for hc in self.harnesses:
114
+ if not hc.name:
115
+ raise ValueError(f"harnesses 配置缺少 name: {hc}")
116
+ cfg = (
117
+ self._apply_harness_overrides(hc, harness_overrides)
118
+ if harness_overrides else hc
119
+ )
120
+ reg.harness(cfg.name, cfg)
121
+ for cc in self.commands:
122
+ if not cc.name:
123
+ raise ValueError(f"commands 配置缺少 name: {cc}")
124
+ reg.command(cc.name, cc)
125
+ for sname, fn in self._scripts.items():
126
+ reg.script(sname)(fn)
127
+ for gname, gfn in self.guards:
128
+ reg.guard(gname, gfn)
129
+ register_builtin_harnesses(reg)
130
+ return reg
131
+
132
+ @staticmethod
133
+ def _apply_harness_overrides(
134
+ hc: HarnessConfig, overrides: dict[str, Any]
135
+ ) -> HarnessConfig:
136
+ """批量应用 LLM 覆盖(model/temperature/think/api_params)到单个 harness。"""
137
+ api_params = dict(hc.api_params)
138
+ if overrides.get("api_params"):
139
+ api_params.update(overrides["api_params"])
140
+ return HarnessConfig(
141
+ name=hc.name,
142
+ prompt_core=hc.prompt_core,
143
+ prompt_modes=dict(hc.prompt_modes),
144
+ output_format=hc.output_format,
145
+ notdo=list(hc.notdo),
146
+ model=overrides.get("model", hc.model),
147
+ temperature=overrides.get("temperature", hc.temperature),
148
+ think=overrides.get("think", hc.think),
149
+ api_params=api_params,
150
+ )
151
+
152
+ async def run(
153
+ self,
154
+ spec: dict[str, Any],
155
+ *,
156
+ tasklist: Tasklist | dict[str, Any] | None = None,
157
+ audit: bool = False,
158
+ max_ticks: int = 100,
159
+ harness_overrides: dict[str, Any] | None = None,
160
+ persist: bool | None = None,
161
+ llm_client: Any = None,
162
+ event_bus: EventBus | None = None,
163
+ hooks: dict | None = None,
164
+ ) -> list[Any]:
165
+ """执行 submodule。
166
+
167
+ - tasklist=None:用自身固定 tasklist,不触发一致性审核(发布前已验证)
168
+ - 传入自定义 tasklist:与 Module 一致,校验 + 一致性审核
169
+ - harness_overrides:{model/temperature/think/api_params} 覆盖,
170
+ 构建 registry 时应用到 submodule 自身的全部 harness(不含内置
171
+ harness)(submodule 节点 LLM 配置传播)
172
+ - audit=False(默认):嵌入模式,keep_records=False;除非 mode="fast",
173
+ 嵌入模式同样落盘(D11)
174
+ - audit=True:keep_records 全开(全量审计轨迹)
175
+ - 事件投递与 records/persist 解耦:构造传入 event_bus 时事件始终投递
176
+ (与 audit 取值无关);未传则静默 EventBus.null()(嵌入零开销)。宿主
177
+ 需失败原因等现场反馈时,传 event_bus 选择性订阅即可,无需开启审计
178
+ - persist:False = 快速模式(NullBackend 全内存 + 无 status.json,
179
+ 零落盘零 I/O);None = 按 mode 决定("fast" → False,否则 True)
180
+ - llm_client/event_bus:覆盖实例级注入(宿主进程传入);None 用实例值
181
+ - hooks:runner hooks 透传(观察通道,与 Module hooks 同语义)
182
+ """
183
+ errors = self.spec_schema.validate(spec)
184
+ if errors:
185
+ raise SpecValidationError(errors)
186
+ if self.tasklist is None and tasklist is None:
187
+ raise ValueError(f"submodule '{self.name}' 未定义 tasklist")
188
+ use_tasklist = self.tasklist if tasklist is None else tasklist
189
+ if isinstance(use_tasklist, dict):
190
+ use_tasklist = Tasklist.from_json(use_tasklist)
191
+ review = None if tasklist is None else "spec_tasklist_review"
192
+ use_persist = persist if persist is not None else (self.mode != "fast")
193
+ use_client = llm_client if llm_client is not None else self._ensure_client()
194
+ use_bus = event_bus if event_bus is not None else self._event_bus
195
+ reg = self._build_registry(audit, harness_overrides, llm_client=use_client, event_bus=use_bus)
196
+ module = Module(
197
+ spec=spec,
198
+ tasklist=use_tasklist,
199
+ llm_client=use_client,
200
+ event_bus=use_bus,
201
+ module_id=self._module_id(),
202
+ registry=reg,
203
+ review_harness=review,
204
+ keep_records=audit,
205
+ persist=use_persist,
206
+ status_file=use_persist,
207
+ modules=self.modules,
208
+ hooks=hooks,
209
+ )
210
+ return await module.run(max_ticks=max_ticks)
211
+
212
+ def pack(self, out_dir: str | Path) -> Path:
213
+ """导出发布目录:module.json + harnesses/ + scripts/ + commands/。
214
+
215
+ scripts/*.py = 函数源码 + 必要 import(含 @script 装饰器行),
216
+ 加载时 exec 后按函数名取注册,pack/load round-trip 无签名改写。
217
+ """
218
+ if not self.name:
219
+ raise ValueError("submodule 缺少 name,无法打包")
220
+ if self.tasklist is None:
221
+ raise ValueError(f"submodule '{self.name}' 未定义 tasklist,无法打包")
222
+ p = Path(out_dir)
223
+ (p / "harnesses").mkdir(parents=True, exist_ok=True)
224
+ (p / "scripts").mkdir(exist_ok=True)
225
+ (p / "commands").mkdir(exist_ok=True)
226
+ (p / "guards").mkdir(exist_ok=True)
227
+ (p / "submodules").mkdir(exist_ok=True)
228
+ manifest = {
229
+ "name": self.name,
230
+ "version": self.version,
231
+ "description": self.description,
232
+ "submodule": True,
233
+ "spec_schema": asdict(self.spec_schema),
234
+ "requires": list(self.requires),
235
+ "modules": list(self.modules),
236
+ "tasklist": self.tasklist.to_dict(),
237
+ }
238
+ (p / "module.json").write_text(
239
+ json.dumps(manifest, ensure_ascii=False, indent=2), encoding="utf-8"
240
+ )
241
+ for hc in self.harnesses:
242
+ if not hc.name:
243
+ raise ValueError(f"harnesses 配置缺少 name: {hc}")
244
+ (p / "harnesses" / f"{hc.name}.json").write_text(
245
+ json.dumps(hc.to_dict(), ensure_ascii=False, indent=2), encoding="utf-8"
246
+ )
247
+ for cc in self.commands:
248
+ if not cc.name:
249
+ raise ValueError(f"commands 配置缺少 name: {cc}")
250
+ (p / "commands" / f"{cc.name}.json").write_text(
251
+ json.dumps(cc.to_dict(), ensure_ascii=False, indent=2), encoding="utf-8"
252
+ )
253
+ for sname, fn in self._scripts.items():
254
+ src = textwrap.dedent(inspect.getsource(fn))
255
+ header = "from __future__ import annotations\nfrom module_harness.submodule import script\n\n"
256
+ (p / "scripts" / f"{sname}.py").write_text(header + src, encoding="utf-8")
257
+ for gname, gfn in self.guards:
258
+ if gname != gfn.__name__:
259
+ raise ValueError(
260
+ f"guard 注册名 '{gname}' 与函数名 '{gfn.__name__}' 不一致"
261
+ "(注册名 = 打包文件名 = 加载键,与 @script 同约定)"
262
+ )
263
+ src = textwrap.dedent(inspect.getsource(gfn))
264
+ header = "from __future__ import annotations\n\n"
265
+ (p / "guards" / f"{gname}.py").write_text(header + src, encoding="utf-8")
266
+ for mname, mcls in self.modules.items():
267
+ mcls().pack(p / "submodules" / mname)
268
+ return p
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "codereview",
3
+ "description": "代码审查模块:并行审查 + 规范检查后合并报告",
4
+ "translation": {
5
+ "type": "harness",
6
+ "harness": "spec_to_tasklist",
7
+ "prompt": "你是一个流程设计器。根据以下 spec 生成合法的 tasklist JSON。\n\nspec 内容:\n{spec}\n\n生成的 tasklist 应包含三个节点:A 执行代码审查(type=harness, harness=review_code),B 执行规范检查(type=harness, harness=check_rules),C 合并报告(type=script, script=merge_review)。A 和 B 并行执行后同时汇入 C。\n\n每个 Task 必须包含 inputs 字段。示例格式:\n{\"Tasks\": {\"A\": {\"type\": \"harness\", \"harness\": \"review_code\", \"inputs\": {\"code\": \"{spec.code}\", \"language\": \"{spec.language}\"}}, \"B\": {\"type\": \"harness\", \"harness\": \"check_rules\", \"inputs\": {\"code\": \"{spec.code}\", \"rules\": \"{spec.rules}\"}}, \"C\": {\"type\": \"script\", \"script\": \"merge_review\", \"inputs\": {\"review\": \"A\", \"rules\": \"B\"}}}, \"Flow\": \"[A] --> B\\n[A] --> C\\nB --> C\"}\n\n请输出完整的 tasklist JSON,Tasks 键与 Flow 字符串。",
8
+ "prompt_core": "你是一个 tasklist JSON 生成器。根据用户提供的 spec 和额外指令生成合法的 tasklist JSON。\n\n额外指令:\n{prompt_extra}\n\n请严格按照 tasklist JSON 格式输出,包含 Tasks 和 Flow 字段。"
9
+ },
10
+ "tasklist": {
11
+ "Tasks": {
12
+ "A": {
13
+ "type": "harness",
14
+ "harness": "review_code",
15
+ "inputs": {"code": "{spec.code}", "language": "{spec.language}"},
16
+ "outputformat": {"type": "json_object"}
17
+ },
18
+ "B": {
19
+ "type": "harness",
20
+ "harness": "check_rules",
21
+ "inputs": {"code": "{spec.code}", "rules": "{spec.rules}"},
22
+ "outputformat": {"type": "json_object"}
23
+ },
24
+ "C": {
25
+ "type": "script",
26
+ "script": "merge_review",
27
+ "inputs": {"review": "A", "rules": "B"}
28
+ }
29
+ },
30
+ "Flow": "[A] --> B\n[A] --> C\nB --> C"
31
+ }
32
+ }
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "docwrite",
3
+ "description": "文档写作模块:大纲 → 分节写作 → 合并成文",
4
+ "translation": {
5
+ "type": "script",
6
+ "script": "docwrite_translator"
7
+ },
8
+ "tasklist": {
9
+ "Tasks": {
10
+ "A": {
11
+ "type": "harness",
12
+ "harness": "write_outline",
13
+ "inputs": {"topic": "{spec.topic}"},
14
+ "outputformat": {"type": "json_object"}
15
+ },
16
+ "B": {
17
+ "type": "harness",
18
+ "harness": "write_section",
19
+ "inputs": {"outline": "A"},
20
+ "outputformat": {"type": "json_object"}
21
+ },
22
+ "C": {
23
+ "type": "script",
24
+ "script": "merge_doc",
25
+ "inputs": {"outline": "A", "sections": "B"}
26
+ }
27
+ },
28
+ "Flow": "A --> B\nB --> C"
29
+ }
30
+ }
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "summarize",
3
+ "description": "文本总结模块:提取要点 + 压缩摘要",
4
+ "translation": {
5
+ "type": "script",
6
+ "script": "summarize_translator"
7
+ },
8
+ "tasklist": {
9
+ "Tasks": {
10
+ "A": {
11
+ "type": "harness",
12
+ "harness": "summarize",
13
+ "inputs": {"text": "{spec.text}"},
14
+ "outputformat": {"type": "json_object"}
15
+ },
16
+ "B": {
17
+ "type": "script",
18
+ "script": "format_summary",
19
+ "inputs": {"data": "A"}
20
+ }
21
+ },
22
+ "Flow": "A --> B"
23
+ }
24
+ }
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "translate",
3
+ "description": "通用翻译模块",
4
+ "translation": {
5
+ "type": "harness",
6
+ "harness": "spec_to_tasklist",
7
+ "prompt": "你是一个流程设计器。根据以下 spec 生成合法的 tasklist JSON。spec 包含 task_type、source_text、target_lang、style 字段。生成的 tasklist 应包含两个节点:A 执行翻译(type=harness, harness=translate),B 执行后处理(type=script, script=format_output)。请输出完整的 tasklist JSON,Tasks 键与 Flow 字符串。",
8
+ "prompt_core": "你是一个 tasklist JSON 生成器。根据用户提供的 spec 和额外指令生成合法的 tasklist JSON。\n\n额外指令:\n{prompt_extra}\n\n请严格按照 tasklist JSON 格式输出,包含 Tasks 和 Flow 字段。"
9
+ },
10
+ "tasklist": {
11
+ "Tasks": {
12
+ "A": {
13
+ "type": "harness",
14
+ "harness": "translate",
15
+ "promptmode": "{spec.style}",
16
+ "inputs": {"text": "{spec.source_text}"},
17
+ "outputformat": {"type": "json_object"}
18
+ },
19
+ "B": {
20
+ "type": "script",
21
+ "script": "format_output",
22
+ "inputs": {"data": "A"}
23
+ }
24
+ },
25
+ "Flow": "A --> B"
26
+ }
27
+ }