specmodule 0.1.3__tar.gz → 0.1.4__tar.gz
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.
- {specmodule-0.1.3/specmodule.egg-info → specmodule-0.1.4}/PKG-INFO +1 -1
- {specmodule-0.1.3 → specmodule-0.1.4}/module_harness/__init__.py +11 -0
- specmodule-0.1.4/module_harness/call.py +89 -0
- {specmodule-0.1.3 → specmodule-0.1.4}/module_harness/consistency.py +28 -35
- {specmodule-0.1.3 → specmodule-0.1.4}/module_harness/module.py +86 -25
- {specmodule-0.1.3 → specmodule-0.1.4}/module_harness/query.py +196 -0
- {specmodule-0.1.3 → specmodule-0.1.4}/module_harness/registry.py +9 -0
- {specmodule-0.1.3 → specmodule-0.1.4}/module_harness/status.py +1 -1
- specmodule-0.1.4/module_harness/stream.py +65 -0
- {specmodule-0.1.3 → specmodule-0.1.4}/module_harness/submodule.py +9 -2
- {specmodule-0.1.3 → specmodule-0.1.4}/pyproject.toml +10 -2
- {specmodule-0.1.3 → specmodule-0.1.4/specmodule.egg-info}/PKG-INFO +1 -1
- {specmodule-0.1.3 → specmodule-0.1.4}/specmodule.egg-info/SOURCES.txt +2 -0
- {specmodule-0.1.3 → specmodule-0.1.4}/LICENSE +0 -0
- {specmodule-0.1.3 → specmodule-0.1.4}/README.md +0 -0
- {specmodule-0.1.3 → specmodule-0.1.4}/llm/__init__.py +0 -0
- {specmodule-0.1.3 → specmodule-0.1.4}/llm/client.py +0 -0
- {specmodule-0.1.3 → specmodule-0.1.4}/llm/config.py +0 -0
- {specmodule-0.1.3 → specmodule-0.1.4}/llm/mock.py +0 -0
- {specmodule-0.1.3 → specmodule-0.1.4}/module_harness/align.py +0 -0
- {specmodule-0.1.3 → specmodule-0.1.4}/module_harness/builtins.py +0 -0
- {specmodule-0.1.3 → specmodule-0.1.4}/module_harness/checkpoint.py +0 -0
- {specmodule-0.1.3 → specmodule-0.1.4}/module_harness/cli.py +0 -0
- {specmodule-0.1.3 → specmodule-0.1.4}/module_harness/command.py +0 -0
- {specmodule-0.1.3 → specmodule-0.1.4}/module_harness/config.py +0 -0
- {specmodule-0.1.3 → specmodule-0.1.4}/module_harness/control.py +0 -0
- {specmodule-0.1.3 → specmodule-0.1.4}/module_harness/entry.py +0 -0
- {specmodule-0.1.3 → specmodule-0.1.4}/module_harness/events.py +0 -0
- {specmodule-0.1.3 → specmodule-0.1.4}/module_harness/feed.py +0 -0
- {specmodule-0.1.3 → specmodule-0.1.4}/module_harness/graph_builder.py +0 -0
- {specmodule-0.1.3 → specmodule-0.1.4}/module_harness/harness.py +0 -0
- {specmodule-0.1.3 → specmodule-0.1.4}/module_harness/loader.py +0 -0
- {specmodule-0.1.3 → specmodule-0.1.4}/module_harness/outputfmt.py +0 -0
- {specmodule-0.1.3 → specmodule-0.1.4}/module_harness/prompt.py +0 -0
- {specmodule-0.1.3 → specmodule-0.1.4}/module_harness/scaffold.py +0 -0
- {specmodule-0.1.3 → specmodule-0.1.4}/module_harness/spec.py +0 -0
- {specmodule-0.1.3 → specmodule-0.1.4}/module_harness/store.py +0 -0
- {specmodule-0.1.3 → specmodule-0.1.4}/module_harness/templates/builtin/codereview.json +0 -0
- {specmodule-0.1.3 → specmodule-0.1.4}/module_harness/templates/builtin/docwrite.json +0 -0
- {specmodule-0.1.3 → specmodule-0.1.4}/module_harness/templates/builtin/summarize.json +0 -0
- {specmodule-0.1.3 → specmodule-0.1.4}/module_harness/templates/builtin/translate.json +0 -0
- {specmodule-0.1.3 → specmodule-0.1.4}/module_harness/translator.py +0 -0
- {specmodule-0.1.3 → specmodule-0.1.4}/setup.cfg +0 -0
- {specmodule-0.1.3 → specmodule-0.1.4}/specmodule.egg-info/dependency_links.txt +0 -0
- {specmodule-0.1.3 → specmodule-0.1.4}/specmodule.egg-info/entry_points.txt +0 -0
- {specmodule-0.1.3 → specmodule-0.1.4}/specmodule.egg-info/requires.txt +0 -0
- {specmodule-0.1.3 → specmodule-0.1.4}/specmodule.egg-info/top_level.txt +0 -0
|
@@ -24,6 +24,7 @@ from .events import (
|
|
|
24
24
|
)
|
|
25
25
|
from .command import Command, CommandConfig
|
|
26
26
|
from .harness import Harness
|
|
27
|
+
from .call import HarnessCallError, HarnessCallResult, call_harness
|
|
27
28
|
from .registry import HarnessRegistry
|
|
28
29
|
from .spec import (
|
|
29
30
|
Spec,
|
|
@@ -57,6 +58,7 @@ from .query import (
|
|
|
57
58
|
ReviewTimeline,
|
|
58
59
|
build_checkpoints,
|
|
59
60
|
build_timeline,
|
|
61
|
+
check_resume_compat_from_run,
|
|
60
62
|
checkpoints_to_dict,
|
|
61
63
|
create_checkpoint,
|
|
62
64
|
filter_failed,
|
|
@@ -95,6 +97,10 @@ from .store import (
|
|
|
95
97
|
validate_pack_dir,
|
|
96
98
|
)
|
|
97
99
|
|
|
100
|
+
# 嵌入者最小面(用法见 docs/guides/embedding.md):
|
|
101
|
+
# task 级 = call_harness / HarnessCallResult / HarnessCallError
|
|
102
|
+
# 图级 = Module / HarnessRegistry + HarnessConfig / OutputFormat / EventBus
|
|
103
|
+
# + TemplateLoader + register_builtin_harnesses
|
|
98
104
|
__all__ = [
|
|
99
105
|
# 配置
|
|
100
106
|
"HarnessConfig",
|
|
@@ -118,6 +124,10 @@ __all__ = [
|
|
|
118
124
|
"ScriptFailed",
|
|
119
125
|
# 核心
|
|
120
126
|
"Harness",
|
|
127
|
+
# task 级 API 地板(嵌入者消费面,docs/dev/superpowers/specs/2026-09-01-embedder-face-design.md)
|
|
128
|
+
"HarnessCallResult",
|
|
129
|
+
"HarnessCallError",
|
|
130
|
+
"call_harness",
|
|
121
131
|
"HarnessRegistry",
|
|
122
132
|
"Command",
|
|
123
133
|
"CommandConfig",
|
|
@@ -168,6 +178,7 @@ __all__ = [
|
|
|
168
178
|
"ResumeCheck",
|
|
169
179
|
"ResumeError",
|
|
170
180
|
"check_resume_compat",
|
|
181
|
+
"check_resume_compat_from_run",
|
|
171
182
|
# 模块入口(roadmap Phase 0:CLI 使用)
|
|
172
183
|
"ModuleEntry",
|
|
173
184
|
"discover_modules",
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# module_harness/call.py
|
|
2
|
+
"""task 级 API 地板 —— 独立调用 harness(嵌入者消费面)。
|
|
3
|
+
|
|
4
|
+
API 金字塔自此 task → graph → run:嵌入者一次函数调用即得 harness 节点的
|
|
5
|
+
全部执行语义(三层 prompt / 输出校验 / 事件),不经图与 run。
|
|
6
|
+
零新执行语义:内部即 Harness.build_body + 一次 body 调用,仅一份执行配方。
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
from tickflow import Failure
|
|
15
|
+
from tickflow.views import DictView, Resolved
|
|
16
|
+
|
|
17
|
+
from .config import HarnessConfig
|
|
18
|
+
from .events import EventBus
|
|
19
|
+
from .harness import Harness
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class HarnessCallResult:
|
|
24
|
+
"""独立调用结果:校验后输出 + LLM 原始输出 + token 用量。"""
|
|
25
|
+
|
|
26
|
+
value: Any # 校验后的输出(json_object → 解析值;text → str)
|
|
27
|
+
raw: str # LLM 原始输出(审计链)
|
|
28
|
+
usage: dict[str, int] # token 用量
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class HarnessCallError(RuntimeError):
|
|
32
|
+
"""独立调用失败(LLM 错误 / 输出不合法)。异常即审计:携带诊断链。"""
|
|
33
|
+
|
|
34
|
+
def __init__(
|
|
35
|
+
self,
|
|
36
|
+
failure: Failure,
|
|
37
|
+
*,
|
|
38
|
+
prompt: str | None = None,
|
|
39
|
+
raw: str | None = None,
|
|
40
|
+
usage: dict[str, int] | None = None,
|
|
41
|
+
) -> None:
|
|
42
|
+
self.failure = failure
|
|
43
|
+
self.prompt = prompt
|
|
44
|
+
self.raw = raw
|
|
45
|
+
self.usage = usage
|
|
46
|
+
super().__init__(failure.error)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
async def call_harness(
|
|
50
|
+
config: HarnessConfig,
|
|
51
|
+
values: dict[str, Any],
|
|
52
|
+
*,
|
|
53
|
+
llm_client: Any,
|
|
54
|
+
promptmode: str | None = None,
|
|
55
|
+
prompt_extra: str | None = None,
|
|
56
|
+
event_bus: EventBus | None = None,
|
|
57
|
+
) -> HarnessCallResult:
|
|
58
|
+
"""独立调用一个 harness:一次函数调用拿到校验后的输出。
|
|
59
|
+
|
|
60
|
+
``values``:prompt 占位符取值 {key: value}。task 层的占位符兜底就是它
|
|
61
|
+
(无 spec_inputs / input_aliases —— 那些是图概念)。
|
|
62
|
+
|
|
63
|
+
``event_bus``:传则收全套 harness 事件(PromptRendered / LlmToken /
|
|
64
|
+
OutputValidated / ...),不传零开销(EventBus.null())。
|
|
65
|
+
|
|
66
|
+
失败(LLM 错误 / 输出校验不通过)抛 HarnessCallError,携带 failure 与
|
|
67
|
+
渲染 prompt / 原始输出 / usage 诊断链。task 层没有"下游跳过"概念,
|
|
68
|
+
Failure 一律翻译为异常;promptmode 缺 key → KeyError 原样冒出。
|
|
69
|
+
"""
|
|
70
|
+
bus = event_bus or EventBus.null()
|
|
71
|
+
body = Harness(config, llm_client, bus).build_body(
|
|
72
|
+
promptmode=promptmode,
|
|
73
|
+
prompt_extra=prompt_extra,
|
|
74
|
+
)
|
|
75
|
+
state: dict[str, Any] = {}
|
|
76
|
+
view = DictView(
|
|
77
|
+
{key: Resolved(value=val, k=None) for key, val in values.items()},
|
|
78
|
+
state=state,
|
|
79
|
+
node="__call__",
|
|
80
|
+
)
|
|
81
|
+
result = await body(view)
|
|
82
|
+
|
|
83
|
+
prompt = state.get("_prompt")
|
|
84
|
+
raw = state.get("_llm_raw")
|
|
85
|
+
usage = state.get("_usage")
|
|
86
|
+
|
|
87
|
+
if isinstance(result, Failure):
|
|
88
|
+
raise HarnessCallError(result, prompt=prompt, raw=raw, usage=usage)
|
|
89
|
+
return HarnessCallResult(value=result, raw=raw, usage=usage)
|
|
@@ -1,18 +1,16 @@
|
|
|
1
1
|
# module_harness/consistency.py
|
|
2
2
|
"""一致性审核 — spec + tasklist 语义一致性检查。
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
独立于翻译通道:审核不经过模板,经 call_harness 直接调用注册的审核
|
|
5
|
+
harness 配置(不走 tickflow 图)。
|
|
5
6
|
"""
|
|
6
7
|
|
|
7
8
|
from __future__ import annotations
|
|
8
9
|
|
|
9
10
|
import json
|
|
10
11
|
from dataclasses import dataclass
|
|
11
|
-
from typing import Any
|
|
12
|
-
|
|
13
|
-
from tickflow import Failure
|
|
14
|
-
from tickflow.views import DictView, Resolved
|
|
15
12
|
|
|
13
|
+
from .call import HarnessCallError, call_harness
|
|
16
14
|
from .config import HarnessConfig
|
|
17
15
|
from .outputfmt import OutputFormat
|
|
18
16
|
from .registry import HarnessRegistry
|
|
@@ -60,7 +58,14 @@ def register_review_harness(
|
|
|
60
58
|
|
|
61
59
|
|
|
62
60
|
class ConsistencyReviewer:
|
|
63
|
-
"""调用审核 harness
|
|
61
|
+
"""调用审核 harness,返回 ConsistencyReport。
|
|
62
|
+
|
|
63
|
+
审核走 call_harness(task 级地板):不传 bus,内部中间事件静默
|
|
64
|
+
(ConsistencyReviewed 领域事件由 Module 直接发射,不经此处)。
|
|
65
|
+
按 register_review_harness 契约,审核 harness 只带 config 注册
|
|
66
|
+
(注册期 promptmode/prompt_extra/spec_inputs/input_aliases 对审核器无意义,
|
|
67
|
+
call_harness 路径不传)。
|
|
68
|
+
"""
|
|
64
69
|
|
|
65
70
|
def __init__(
|
|
66
71
|
self, registry: HarnessRegistry, harness_name: str = "spec_tasklist_review"
|
|
@@ -70,41 +75,32 @@ class ConsistencyReviewer:
|
|
|
70
75
|
|
|
71
76
|
async def review(self, spec: Spec, tasklist: Tasklist) -> ConsistencyReport:
|
|
72
77
|
"""执行一致性审核。审核失败(LLM 错误/输出不合法)抛 ValueError。"""
|
|
73
|
-
|
|
78
|
+
config = self.reg.harness_config(self.harness_name)
|
|
79
|
+
if config is None:
|
|
74
80
|
raise ValueError(
|
|
75
81
|
f"审核 harness '{self.harness_name}' 未注册。"
|
|
76
82
|
f"请先调用 register_review_harness(reg) 注册内置审核器,"
|
|
77
83
|
f"或自行 reg.harness('{self.harness_name}', ...)。"
|
|
78
84
|
)
|
|
79
|
-
body = self.reg.get_body(self.harness_name)
|
|
80
|
-
|
|
81
|
-
tasklist_dict = tasklist.to_dict()
|
|
82
|
-
state: dict[str, Any] = {}
|
|
83
|
-
view = DictView(
|
|
84
|
-
{
|
|
85
|
-
"spec": Resolved(value=spec.to_dict(), k=None),
|
|
86
|
-
"tasklist": Resolved(
|
|
87
|
-
value=json.dumps(tasklist_dict, ensure_ascii=False), k=None
|
|
88
|
-
),
|
|
89
|
-
},
|
|
90
|
-
state=state,
|
|
91
|
-
node="__review__",
|
|
92
|
-
)
|
|
93
|
-
result = await body(view)
|
|
94
85
|
|
|
95
|
-
|
|
96
|
-
|
|
86
|
+
try:
|
|
87
|
+
call = await call_harness(
|
|
88
|
+
config,
|
|
89
|
+
{
|
|
90
|
+
"spec": spec.to_dict(),
|
|
91
|
+
"tasklist": json.dumps(tasklist.to_dict(), ensure_ascii=False),
|
|
92
|
+
},
|
|
93
|
+
llm_client=self.reg.llm_client,
|
|
94
|
+
)
|
|
95
|
+
except HarnessCallError as e:
|
|
96
|
+
raise ValueError(f"审核 harness 返回 Failure: {e.failure.error}") from e
|
|
97
97
|
|
|
98
|
-
|
|
98
|
+
data = call.value
|
|
99
|
+
if isinstance(data, str):
|
|
99
100
|
try:
|
|
100
|
-
data = json.loads(
|
|
101
|
+
data = json.loads(data)
|
|
101
102
|
except json.JSONDecodeError as e:
|
|
102
103
|
raise ValueError(f"审核输出不是合法 JSON: {e}") from e
|
|
103
|
-
elif isinstance(result, dict):
|
|
104
|
-
data = result
|
|
105
|
-
else:
|
|
106
|
-
raise ValueError(f"审核输出类型异常: {type(result).__name__}")
|
|
107
|
-
|
|
108
104
|
if not isinstance(data, dict):
|
|
109
105
|
raise ValueError(f"审核输出必须是 JSON 对象: {data!r}")
|
|
110
106
|
|
|
@@ -115,9 +111,6 @@ class ConsistencyReviewer:
|
|
|
115
111
|
if not isinstance(suggestions, str):
|
|
116
112
|
raise ValueError(f"审核输出 'suggestions' 必须是字符串: {data!r}")
|
|
117
113
|
|
|
118
|
-
raw = state.get("_llm_raw")
|
|
119
|
-
if raw is None:
|
|
120
|
-
raw = result if isinstance(result, str) else json.dumps(data, ensure_ascii=False)
|
|
121
114
|
return ConsistencyReport(
|
|
122
|
-
consistent=consistent, suggestions=suggestions, raw=raw
|
|
115
|
+
consistent=consistent, suggestions=suggestions, raw=call.raw
|
|
123
116
|
)
|
|
@@ -20,7 +20,14 @@ from .consistency import ConsistencyError, ConsistencyReport, ConsistencyReviewe
|
|
|
20
20
|
from .translator import Translator, TemplateLoader, TasklistValidator
|
|
21
21
|
from .graph_builder import TasklistTranslator
|
|
22
22
|
from .registry import HarnessRegistry
|
|
23
|
-
from .events import
|
|
23
|
+
from .events import (
|
|
24
|
+
EventBus,
|
|
25
|
+
ConsistencyReviewed,
|
|
26
|
+
HarnessFailed,
|
|
27
|
+
LlmCallCompleted,
|
|
28
|
+
LlmCallStarted,
|
|
29
|
+
LlmToken,
|
|
30
|
+
)
|
|
24
31
|
from .checkpoint import (
|
|
25
32
|
ModuleInputStore,
|
|
26
33
|
ResumeError,
|
|
@@ -28,7 +35,9 @@ from .checkpoint import (
|
|
|
28
35
|
tasklist_from_dict,
|
|
29
36
|
tasklist_to_dict,
|
|
30
37
|
)
|
|
38
|
+
from .query import _executed_nodes
|
|
31
39
|
from .control import clear_control, control_tick_end, control_tick_start
|
|
40
|
+
from .stream import StreamLogWriter, stream_log_path
|
|
32
41
|
|
|
33
42
|
log = logging.getLogger(__name__)
|
|
34
43
|
|
|
@@ -74,6 +83,10 @@ class Module:
|
|
|
74
83
|
persist: bool = True,
|
|
75
84
|
status_file: bool = True,
|
|
76
85
|
control: bool = True,
|
|
86
|
+
# True(默认):LLM 流式输出落盘 stream.log(跨进程流式观测,见
|
|
87
|
+
# stream.py)。EventBus.null() 场景只有 run_start 记录(事件被
|
|
88
|
+
# null bus 吞掉,属预期)。
|
|
89
|
+
stream_log: bool = True,
|
|
77
90
|
modules: dict[str, Any] | None = None,
|
|
78
91
|
hooks: dict | None = None,
|
|
79
92
|
) -> None:
|
|
@@ -93,6 +106,11 @@ class Module:
|
|
|
93
106
|
# True(默认):注册控制文件 hook(control.json → cancel/pause,
|
|
94
107
|
# 见 control.py)。跨进程取消/暂停的协作式通道;False 关闭。
|
|
95
108
|
self.control = control
|
|
109
|
+
self.stream_log = stream_log
|
|
110
|
+
# stream.log 接线状态:订阅只挂一次(防多次执行重复订阅记录翻倍);
|
|
111
|
+
# writer 每次执行重建(_run_with_phases 生命周期),事件回调读当前属性
|
|
112
|
+
self._stream_subscribed = False
|
|
113
|
+
self._stream_writer: StreamLogWriter | None = None
|
|
96
114
|
self.review_result: ConsistencyReport | None = None
|
|
97
115
|
self.module_id = module_id or f"mod_{uuid.uuid4().hex[:8]}"
|
|
98
116
|
self._base_dir = base_dir or Path.cwd()
|
|
@@ -128,7 +146,7 @@ class Module:
|
|
|
128
146
|
"""原子写 status.json(tmp + os.replace)。失败仅 log,不阻断运行。
|
|
129
147
|
|
|
130
148
|
phase 取值:idle/translating/reviewing/building/ready/running/
|
|
131
|
-
done/aborted/cancelled。status_file=False 时不写盘(零残留)。
|
|
149
|
+
done/aborted/cancelled/truncated。status_file=False 时不写盘(零残留)。
|
|
132
150
|
"""
|
|
133
151
|
if not self.status_file:
|
|
134
152
|
return
|
|
@@ -358,19 +376,66 @@ class Module:
|
|
|
358
376
|
# 位于写 running phase 之前——监控方看到 running 才放开控制按钮,
|
|
359
377
|
# 此时清场已完成,清场与首请求的竞态窗口关闭。
|
|
360
378
|
clear_control(self.module_id, base_dir=self._base_dir)
|
|
361
|
-
self.
|
|
362
|
-
self._write_phase("running")
|
|
379
|
+
writer = self._open_stream_log(max_ticks)
|
|
363
380
|
try:
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
381
|
+
self._archive_module_inputs()
|
|
382
|
+
self._write_phase("running")
|
|
383
|
+
try:
|
|
384
|
+
firings = await runner.run_until_idle(max_ticks=max_ticks)
|
|
385
|
+
except asyncio.CancelledError:
|
|
386
|
+
self._write_phase("cancelled", error="cancelled")
|
|
387
|
+
raise
|
|
388
|
+
except Exception as e:
|
|
389
|
+
self._write_phase("aborted", error=str(e))
|
|
390
|
+
raise
|
|
391
|
+
else:
|
|
392
|
+
self._finalize_phase(runner, max_ticks)
|
|
393
|
+
return firings
|
|
394
|
+
finally:
|
|
395
|
+
self._close_stream_log(writer)
|
|
396
|
+
|
|
397
|
+
def _open_stream_log(self, max_ticks: int) -> StreamLogWriter | None:
|
|
398
|
+
"""stream_log=True 时开本次执行的 writer 并写 run_start 边界。
|
|
399
|
+
|
|
400
|
+
run_start 先于 running phase 写入——监控方见到 running 时锚点记录
|
|
401
|
+
必已存在(status.json 原子写在同一执行线程、顺序在后)。事件订阅
|
|
402
|
+
只挂一次(守卫 flag),回调经 Module 当前 writer 属性落盘。
|
|
403
|
+
"""
|
|
404
|
+
if not self.stream_log:
|
|
405
|
+
return None
|
|
406
|
+
if not self._stream_subscribed:
|
|
407
|
+
bus = self._reg._event_bus
|
|
408
|
+
for evt in (LlmCallStarted, LlmToken, LlmCallCompleted, HarnessFailed):
|
|
409
|
+
bus.subscribe(evt, self._on_stream_event)
|
|
410
|
+
self._stream_subscribed = True
|
|
411
|
+
writer = StreamLogWriter(stream_log_path(self.module_id, self._base_dir))
|
|
412
|
+
self._stream_writer = writer
|
|
413
|
+
writer.write({"type": "run_start", "pid": os.getpid(), "max_ticks": max_ticks})
|
|
414
|
+
return writer
|
|
415
|
+
|
|
416
|
+
def _close_stream_log(self, writer: StreamLogWriter | None) -> None:
|
|
417
|
+
"""执行结束(含异常路径)摘除当前 writer 并关闭。"""
|
|
418
|
+
self._stream_writer = None
|
|
419
|
+
if writer is not None:
|
|
420
|
+
writer.close()
|
|
421
|
+
|
|
422
|
+
def _on_stream_event(self, event: Any) -> None:
|
|
423
|
+
"""EventBus → stream.log 记录(四类事件 → 四种记录,见 stream.py)。"""
|
|
424
|
+
w = self._stream_writer
|
|
425
|
+
if w is None:
|
|
426
|
+
return
|
|
427
|
+
if isinstance(event, LlmCallStarted):
|
|
428
|
+
w.write({"type": "call_start", "node": event.node,
|
|
429
|
+
"model": event.model, "prompt_chars": event.prompt_chars})
|
|
430
|
+
elif isinstance(event, LlmToken):
|
|
431
|
+
w.write({"type": "token", "node": event.node, "chunk": event.chunk})
|
|
432
|
+
elif isinstance(event, LlmCallCompleted):
|
|
433
|
+
w.write({"type": "call_end", "node": event.node,
|
|
434
|
+
"content_chars": event.content_chars,
|
|
435
|
+
"finish_reason": event.finish_reason})
|
|
436
|
+
elif isinstance(event, HarnessFailed):
|
|
437
|
+
w.write({"type": "call_error", "node": event.node,
|
|
438
|
+
"reason": event.reason, "failure_type": event.failure_type})
|
|
374
439
|
|
|
375
440
|
def _archive_module_inputs(self) -> None:
|
|
376
441
|
"""归档本次运行的 spec/tasklist 到 module_inputs 表(警告 1 对比源)。
|
|
@@ -387,7 +452,7 @@ class Module:
|
|
|
387
452
|
self.spec.to_dict(), self._last_tasklist.to_dict()
|
|
388
453
|
)
|
|
389
454
|
|
|
390
|
-
def _finalize_phase(self, runner: AsyncRunner) -> None:
|
|
455
|
+
def _finalize_phase(self, runner: AsyncRunner, max_ticks: int) -> None:
|
|
391
456
|
"""按 runner.status 映射终态 phase(run/resume 共用)。"""
|
|
392
457
|
from tickflow.runner import RunStatus
|
|
393
458
|
if runner.status == RunStatus.ABORTED:
|
|
@@ -397,7 +462,11 @@ class Module:
|
|
|
397
462
|
elif runner.status == RunStatus.FAILED:
|
|
398
463
|
self._write_phase("aborted", error="all nodes failed")
|
|
399
464
|
elif runner.status == RunStatus.RUNNING:
|
|
400
|
-
|
|
465
|
+
# max_ticks 耗尽(pause 挂起发生在 run_until_idle 内部不返回,
|
|
466
|
+
# RUNNING 是唯一来源)→ 真终态:监控方拿到可 resume 的确定性信号
|
|
467
|
+
self._write_phase(
|
|
468
|
+
"truncated", error=f"max_ticks={max_ticks} 截断(可 resume 续跑)"
|
|
469
|
+
)
|
|
401
470
|
else:
|
|
402
471
|
self._write_phase("done")
|
|
403
472
|
|
|
@@ -434,15 +503,7 @@ class Module:
|
|
|
434
503
|
f"回退目标 {rollback_to!r} 不存在"
|
|
435
504
|
f"(可用 tick: {ticks or '无'};manual: {manual or '无'})"
|
|
436
505
|
)
|
|
437
|
-
|
|
438
|
-
# 快照不再含 edges 窗口)。快照 tick N 在 tick N-1 结束后落盘,
|
|
439
|
-
# tick == N 的 firing 属于 restore 后会被重跑的部分,不算已执行。
|
|
440
|
-
# 注:firings 表按 module_id 累积(跨多次 run),前一轮 run 的
|
|
441
|
-
# 记录也会计入——仅影响提示性警告 1/3 的准确性,不影响硬错误。
|
|
442
|
-
executed_nodes = {
|
|
443
|
-
d["node"] for d in backend.list_firings(self.module_id)
|
|
444
|
-
if d.get("node") and int(d.get("tick", 0)) < int(snap.get("tick", 0))
|
|
445
|
-
}
|
|
506
|
+
executed_nodes = _executed_nodes(backend, self.module_id, int(snap.get("tick", 0)))
|
|
446
507
|
except KeyError as e:
|
|
447
508
|
self._write_phase("aborted", error=str(e))
|
|
448
509
|
raise
|
|
@@ -8,11 +8,14 @@ CLI(host + 查询形态)、MCP、Web 三形态共同消费本模块——形
|
|
|
8
8
|
|
|
9
9
|
from __future__ import annotations
|
|
10
10
|
|
|
11
|
+
import json
|
|
11
12
|
import logging
|
|
12
13
|
from dataclasses import dataclass, field
|
|
13
14
|
from pathlib import Path
|
|
14
15
|
from typing import Any
|
|
15
16
|
|
|
17
|
+
from .stream import stream_log_path
|
|
18
|
+
|
|
16
19
|
log = logging.getLogger(__name__)
|
|
17
20
|
|
|
18
21
|
|
|
@@ -46,6 +49,65 @@ def run_db_path(module_id: str, base_dir: Path | None = None) -> Path:
|
|
|
46
49
|
return base / ".specmodule" / "runs" / module_id / "run.sqlite"
|
|
47
50
|
|
|
48
51
|
|
|
52
|
+
def read_stream(
|
|
53
|
+
run_id: str, *, offset: int = 0, base_dir: Path | None = None
|
|
54
|
+
) -> dict[str, Any] | None:
|
|
55
|
+
"""增量读 stream.log(LLM 流式观测共享读端;Web WS 追尾/未来 TUI 共用)。
|
|
56
|
+
|
|
57
|
+
二进制 seek(offset) 读到 EOF,按完整行切分;每条记录附行首字节偏移
|
|
58
|
+
``off``(锚定策略——只显示最近一次执行——由消费方据 off 自行定位,展示
|
|
59
|
+
策略不进库)。末尾不完整行不消费(next_offset 停在其行首,等补齐后
|
|
60
|
+
下次读出);JSON 解析失败的行跳过(崩溃瞬间可能出半行);
|
|
61
|
+
``offset > file_size``(文件被替换/重建)钳回 0 自愈。文件缺失 → None。
|
|
62
|
+
|
|
63
|
+
返回 ``{"records": [...], "next_offset": int, "file_size": int}``。
|
|
64
|
+
"""
|
|
65
|
+
path = stream_log_path(run_id, base_dir)
|
|
66
|
+
if not path.exists():
|
|
67
|
+
return None
|
|
68
|
+
try:
|
|
69
|
+
size = path.stat().st_size
|
|
70
|
+
start = offset if offset <= size else 0
|
|
71
|
+
with path.open("rb") as fh:
|
|
72
|
+
fh.seek(start)
|
|
73
|
+
data = fh.read()
|
|
74
|
+
except OSError:
|
|
75
|
+
log.exception("读 stream.log 失败(返回 None): %s", path)
|
|
76
|
+
return None
|
|
77
|
+
nl = data.rfind(b"\n")
|
|
78
|
+
if nl < 0:
|
|
79
|
+
return {"records": [], "next_offset": start, "file_size": size}
|
|
80
|
+
pos = start
|
|
81
|
+
records: list[dict[str, Any]] = []
|
|
82
|
+
for line in data[:nl].split(b"\n"):
|
|
83
|
+
line_off = pos
|
|
84
|
+
pos += len(line) + 1
|
|
85
|
+
if not line.strip():
|
|
86
|
+
continue
|
|
87
|
+
try:
|
|
88
|
+
rec = json.loads(line.decode("utf-8"))
|
|
89
|
+
except (json.JSONDecodeError, UnicodeDecodeError):
|
|
90
|
+
log.warning("跳过损坏的 stream.log 行(off=%d)", line_off)
|
|
91
|
+
continue
|
|
92
|
+
rec["off"] = line_off
|
|
93
|
+
records.append(rec)
|
|
94
|
+
return {"records": records, "next_offset": start + nl + 1, "file_size": size}
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _executed_nodes(backend: Any, module_id: str, tick: int) -> set[str]:
|
|
98
|
+
"""firings 表中 tick < 快照 tick 的去重节点(resume 已执行判定,单一事实源)。
|
|
99
|
+
|
|
100
|
+
快照 tick N 在 tick N-1 结束后落盘,tick == N 的 firing 属 restore 后
|
|
101
|
+
会被重跑的部分,不算已执行。firings 按 module_id 累积(跨多次 run),
|
|
102
|
+
前一轮 run 的记录也会计入——仅影响提示性警告 1/3 的准确性,不影响硬错误。
|
|
103
|
+
"""
|
|
104
|
+
return {
|
|
105
|
+
d["node"]
|
|
106
|
+
for d in backend.list_firings(module_id)
|
|
107
|
+
if d.get("node") and int(d.get("tick", 0)) < tick
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
|
|
49
111
|
def build_timeline(module_id: str, base_dir: Path | None = None) -> ReviewTimeline | None:
|
|
50
112
|
"""从 run.sqlite 构建审阅时间线。无 DB / 读失败 → None。"""
|
|
51
113
|
db_path = run_db_path(module_id, base_dir=base_dir)
|
|
@@ -319,6 +381,140 @@ def read_module_inputs(
|
|
|
319
381
|
store.close()
|
|
320
382
|
|
|
321
383
|
|
|
384
|
+
def check_resume_compat_from_run(
|
|
385
|
+
module_name: str,
|
|
386
|
+
run_id: str,
|
|
387
|
+
*,
|
|
388
|
+
new_tasklist: Any = None,
|
|
389
|
+
target: int | str | None = None,
|
|
390
|
+
base_dir: Path | None = None,
|
|
391
|
+
) -> dict[str, Any] | None:
|
|
392
|
+
"""恢复预检:从运行产物组合材料跑兼容性校验——不 spawn、不写状态。
|
|
393
|
+
|
|
394
|
+
resume(module.py)内联组合的第二消费端收编:目标快照解析 →
|
|
395
|
+
executed_nodes(_executed_nodes)→ 旧输入存档(read_module_inputs)→
|
|
396
|
+
建图(build_run_graph 同款 Mock registry 通道,零 LLM;new_tasklist
|
|
397
|
+
给出时走直渲染通道)→ check_resume_compat(marking.slots / armed_starts
|
|
398
|
+
取自目标快照)。
|
|
399
|
+
|
|
400
|
+
- ``new_tasklist``:dict 或 Tasklist;None = 用归档 tasklist(纯续跑预检)。
|
|
401
|
+
- ``target``:tick 号 / ``"manual:<label>"`` / None(最新快照)。
|
|
402
|
+
|
|
403
|
+
错误契约:run.sqlite 缺失/读失败 → None(查询容错,调用方映射 404);
|
|
404
|
+
tasklist 非法 / 建图失败 → ValueError(消息可直接面向用户,调用方映射
|
|
405
|
+
400);目标解析失败**不 raise**——作为 ``hard_errors[0]`` 返回(消息与
|
|
406
|
+
module.py 的 KeyError 文案一致并附可用清单),``target``/``target_tick``
|
|
407
|
+
为 None。
|
|
408
|
+
|
|
409
|
+
返回 ``{"target": str | None, "target_tick": int | None,
|
|
410
|
+
"executed_nodes": [str], "hard_errors": [str], "warnings": [str]}``。
|
|
411
|
+
"""
|
|
412
|
+
from .checkpoint import check_resume_compat, tasklist_from_dict
|
|
413
|
+
|
|
414
|
+
db_path = run_db_path(run_id, base_dir)
|
|
415
|
+
if not db_path.exists():
|
|
416
|
+
return None
|
|
417
|
+
try:
|
|
418
|
+
from tickflow.persistence import SqliteBackend
|
|
419
|
+
|
|
420
|
+
backend = SqliteBackend(db_path)
|
|
421
|
+
try:
|
|
422
|
+
hard_errors: list[str] = []
|
|
423
|
+
snap: dict[str, Any] | None = None
|
|
424
|
+
target_tick: int | None = None
|
|
425
|
+
ticks = backend.list_snapshots(run_id)
|
|
426
|
+
manual = [lbl for lbl, _ in backend.list_checkpoints(run_id)]
|
|
427
|
+
if not ticks and not manual:
|
|
428
|
+
hard_errors.append(f"无可恢复快照: {run_id}(运行未产生任何 tick 快照)")
|
|
429
|
+
elif isinstance(target, int) or (
|
|
430
|
+
isinstance(target, str) and target.isdigit()
|
|
431
|
+
):
|
|
432
|
+
t = int(target)
|
|
433
|
+
if t in ticks:
|
|
434
|
+
snap = backend.load_snapshot(run_id, t)
|
|
435
|
+
if snap is None:
|
|
436
|
+
hard_errors.append(f"快照 tick {t} 读取失败(数据损坏?)")
|
|
437
|
+
else:
|
|
438
|
+
target_tick = t
|
|
439
|
+
else:
|
|
440
|
+
hard_errors.append(
|
|
441
|
+
f"回退目标 {target!r} 不存在"
|
|
442
|
+
f"(可用 tick: {ticks or '无'};manual: {manual or '无'})"
|
|
443
|
+
)
|
|
444
|
+
elif isinstance(target, str) and target.startswith("manual:"):
|
|
445
|
+
snap = backend.load_checkpoint(run_id, target)
|
|
446
|
+
if snap is None:
|
|
447
|
+
hard_errors.append(
|
|
448
|
+
f"回退目标 {target!r} 不存在"
|
|
449
|
+
f"(可用 tick: {ticks or '无'};manual: {manual or '无'})"
|
|
450
|
+
)
|
|
451
|
+
else:
|
|
452
|
+
target_tick = int(snap.get("tick", 0))
|
|
453
|
+
elif target is not None:
|
|
454
|
+
hard_errors.append(
|
|
455
|
+
f"回退目标 {target!r} 不存在"
|
|
456
|
+
f"(可用 tick: {ticks or '无'};manual: {manual or '无'})"
|
|
457
|
+
)
|
|
458
|
+
else:
|
|
459
|
+
if not ticks:
|
|
460
|
+
hard_errors.append(
|
|
461
|
+
f"无可恢复快照: {run_id}(运行未产生任何 tick 快照)"
|
|
462
|
+
)
|
|
463
|
+
else:
|
|
464
|
+
target_tick = max(ticks)
|
|
465
|
+
snap = backend.load_snapshot(run_id, target_tick)
|
|
466
|
+
if snap is None:
|
|
467
|
+
hard_errors.append(
|
|
468
|
+
f"快照 tick {target_tick} 读取失败(数据损坏?)"
|
|
469
|
+
)
|
|
470
|
+
|
|
471
|
+
executed: set[str] = set()
|
|
472
|
+
if snap is not None:
|
|
473
|
+
executed = _executed_nodes(backend, run_id, int(snap.get("tick", 0)))
|
|
474
|
+
finally:
|
|
475
|
+
backend.close()
|
|
476
|
+
except Exception:
|
|
477
|
+
log.exception("读取 run.sqlite 失败(返回 None): %s", db_path)
|
|
478
|
+
return None
|
|
479
|
+
|
|
480
|
+
if hard_errors:
|
|
481
|
+
return {"target": None, "target_tick": None, "executed_nodes": [],
|
|
482
|
+
"hard_errors": hard_errors, "warnings": []}
|
|
483
|
+
|
|
484
|
+
old_tl = None
|
|
485
|
+
old_inputs = read_module_inputs(run_id, base_dir=base_dir)
|
|
486
|
+
if old_inputs is not None:
|
|
487
|
+
old_tl = tasklist_from_dict(old_inputs["tasklist"])
|
|
488
|
+
|
|
489
|
+
try:
|
|
490
|
+
built = build_run_graph(module_name, run_id, base_dir=base_dir,
|
|
491
|
+
tasklist=new_tasklist)
|
|
492
|
+
except ValueError:
|
|
493
|
+
raise
|
|
494
|
+
except Exception as e:
|
|
495
|
+
raise ValueError(f"预检建图失败: {e}") from e
|
|
496
|
+
if built is None:
|
|
497
|
+
raise ValueError("无 tasklist 可预检(无归档且未传 new_tasklist)")
|
|
498
|
+
graph, new_tl = built
|
|
499
|
+
|
|
500
|
+
marking = (snap or {}).get("marking") or {}
|
|
501
|
+
check = check_resume_compat(
|
|
502
|
+
new_tl, graph, executed,
|
|
503
|
+
old_tasklist=old_tl,
|
|
504
|
+
marking_slots=marking.get("slots"),
|
|
505
|
+
armed_starts=marking.get("armed_starts"),
|
|
506
|
+
)
|
|
507
|
+
target_str = target if isinstance(target, str) else (
|
|
508
|
+
str(target) if target is not None else str(target_tick))
|
|
509
|
+
return {
|
|
510
|
+
"target": target_str,
|
|
511
|
+
"target_tick": target_tick,
|
|
512
|
+
"executed_nodes": sorted(executed),
|
|
513
|
+
"hard_errors": list(check.hard_errors),
|
|
514
|
+
"warnings": list(check.warnings),
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
|
|
322
518
|
def timeline_to_dict(timeline: ReviewTimeline) -> dict[str, Any]:
|
|
323
519
|
"""JSON 出口(MCP/Web 直接消费同一函数)。"""
|
|
324
520
|
return {
|
|
@@ -155,6 +155,15 @@ class HarnessRegistry(Registry):
|
|
|
155
155
|
|
|
156
156
|
# ── 查询 ──────────────────────────────────────────────────────
|
|
157
157
|
|
|
158
|
+
@property
|
|
159
|
+
def llm_client(self) -> Any:
|
|
160
|
+
"""注册表持有的 LLM 客户端(只读)。
|
|
161
|
+
|
|
162
|
+
供不经图独立调用 harness 的场景使用(如 ConsistencyReviewer
|
|
163
|
+
走 call_harness)。
|
|
164
|
+
"""
|
|
165
|
+
return self._llm_client
|
|
166
|
+
|
|
158
167
|
def is_harness(self, name: str) -> bool:
|
|
159
168
|
"""name 是否通过 harness() 注册。"""
|
|
160
169
|
return name in self._harness_cfgs
|
|
@@ -25,7 +25,7 @@ class ModuleStatus:
|
|
|
25
25
|
"""Module 运行状态静态快照。"""
|
|
26
26
|
|
|
27
27
|
module_id: str
|
|
28
|
-
phase: str # idle/translating/reviewing/building/ready/running/done/aborted/cancelled
|
|
28
|
+
phase: str # idle/translating/reviewing/building/ready/running/done/aborted/cancelled/truncated
|
|
29
29
|
status: str | None = None # tickflow RunStatus("running"/"idle"/...;无 DB 时为 None)
|
|
30
30
|
tick: int | None = None # 最新快照 tick(无 DB 时为 None)
|
|
31
31
|
fireable: list[str] = field(default_factory=list)
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# module_harness/stream.py
|
|
2
|
+
"""LLM 流式输出落盘——stream.log(跨进程流式观测通道)。
|
|
3
|
+
|
|
4
|
+
Harness 每个LLM chunk 经 EventBus 发 ``LlmToken``,但 EventBus 是进程内的——
|
|
5
|
+
独立消费进程(Web/TUI)看不到。本模块把流式事件序列化为 JSONL 追加写
|
|
6
|
+
``.specmodule/runs/<run_id>/stream.log``(与 status.json/run.sqlite 同目录),
|
|
7
|
+
跨进程可读。写失败仅 log 不抛(观测不阻断运行,同 _write_phase 哲学)。
|
|
8
|
+
|
|
9
|
+
记录格式(``ts`` 由写入方统一打 wall-clock——harness 事件的 timestamp 是
|
|
10
|
+
``time.monotonic()``,进程本地时钟,不落盘、不跨进程比较)::
|
|
11
|
+
|
|
12
|
+
{"type": "run_start", "ts", "pid", "max_ticks"}
|
|
13
|
+
{"type": "call_start", "ts", "node", "model", "prompt_chars"}
|
|
14
|
+
{"type": "token", "ts", "node", "chunk"}
|
|
15
|
+
{"type": "call_end", "ts", "node", "content_chars", "finish_reason"}
|
|
16
|
+
{"type": "call_error", "ts", "node", "reason", "failure_type"}
|
|
17
|
+
|
|
18
|
+
append-only:每次执行以 ``run_start`` 开边界,不截断旧执行(崩溃残留可事后
|
|
19
|
+
查看)。不带 tick 字段——harness 事件恒为 tick=0,落盘假数据不如不写。
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import json
|
|
25
|
+
import logging
|
|
26
|
+
import time
|
|
27
|
+
from pathlib import Path
|
|
28
|
+
from typing import Any
|
|
29
|
+
|
|
30
|
+
log = logging.getLogger(__name__)
|
|
31
|
+
|
|
32
|
+
__all__ = ["StreamLogWriter", "stream_log_path"]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def stream_log_path(module_id: str, base_dir: Path | None = None) -> Path:
|
|
36
|
+
"""``<base_dir>/.specmodule/runs/<module_id>/stream.log``。"""
|
|
37
|
+
return (base_dir or Path.cwd()) / ".specmodule" / "runs" / module_id / "stream.log"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class StreamLogWriter:
|
|
41
|
+
"""stream.log 追加写器:懒开句柄、逐记录 flush(无 fsync)、close 幂等。"""
|
|
42
|
+
|
|
43
|
+
def __init__(self, path: Path) -> None:
|
|
44
|
+
self._path = path
|
|
45
|
+
self._fh: Any = None
|
|
46
|
+
|
|
47
|
+
def write(self, record: dict[str, Any]) -> None:
|
|
48
|
+
"""单条 JSONL 记录落盘(ts 在此统一打 wall-clock)。失败仅 log。"""
|
|
49
|
+
record = {"ts": time.time(), **record}
|
|
50
|
+
try:
|
|
51
|
+
if self._fh is None:
|
|
52
|
+
self._path.parent.mkdir(parents=True, exist_ok=True)
|
|
53
|
+
self._fh = self._path.open("a", encoding="utf-8")
|
|
54
|
+
self._fh.write(json.dumps(record, ensure_ascii=False) + "\n")
|
|
55
|
+
self._fh.flush()
|
|
56
|
+
except OSError:
|
|
57
|
+
log.exception("写 stream.log 失败(不阻断运行): %s", self._path)
|
|
58
|
+
|
|
59
|
+
def close(self) -> None:
|
|
60
|
+
if self._fh is not None:
|
|
61
|
+
try:
|
|
62
|
+
self._fh.close()
|
|
63
|
+
except OSError:
|
|
64
|
+
log.exception("关闭 stream.log 失败: %s", self._path)
|
|
65
|
+
self._fh = None
|
|
@@ -175,8 +175,9 @@ class SubModule:
|
|
|
175
175
|
- 事件投递与 records/persist 解耦:构造传入 event_bus 时事件始终投递
|
|
176
176
|
(与 audit 取值无关);未传则静默 EventBus.null()(嵌入零开销)。宿主
|
|
177
177
|
需失败原因等现场反馈时,传 event_bus 选择性订阅即可,无需开启审计
|
|
178
|
-
- persist:False = 快速模式(NullBackend 全内存 + 无 status.json
|
|
179
|
-
|
|
178
|
+
- persist:False = 快速模式(NullBackend 全内存 + 无 status.json +
|
|
179
|
+
无 stream.log,零落盘零 I/O);None = 按 mode 决定("fast" → False,
|
|
180
|
+
否则 True)
|
|
180
181
|
- llm_client/event_bus:覆盖实例级注入(宿主进程传入);None 用实例值
|
|
181
182
|
- hooks:runner hooks 透传(观察通道,与 Module hooks 同语义)
|
|
182
183
|
"""
|
|
@@ -204,6 +205,12 @@ class SubModule:
|
|
|
204
205
|
keep_records=audit,
|
|
205
206
|
persist=use_persist,
|
|
206
207
|
status_file=use_persist,
|
|
208
|
+
# fast 模式 = 零残留模式:三个落盘通道(run.sqlite /
|
|
209
|
+
# status.json / stream.log)由 mode 一并关闭。stream_log 的
|
|
210
|
+
# 默认 True 是刻意的(CLI 拉起的子进程零接线即可流式观测),
|
|
211
|
+
# 故只在具名模式侧统一关——直接构造 Module 无"模式"概念,
|
|
212
|
+
# 每个通道由调用方逐个点名。
|
|
213
|
+
stream_log=use_persist,
|
|
207
214
|
modules=self.modules,
|
|
208
215
|
hooks=hooks,
|
|
209
216
|
)
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "specmodule"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.4"
|
|
8
8
|
description = "可审计、可调试、可完全掌控的 LLM 使用框架(tickflow + llm + module_harness)"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -41,4 +41,12 @@ include = ["llm*", "module_harness*"]
|
|
|
41
41
|
exclude = ["module_harness.tests*", "llm.tests*"]
|
|
42
42
|
|
|
43
43
|
[tool.setuptools.package-data]
|
|
44
|
-
module_harness = ["templates/builtin/*.json"]
|
|
44
|
+
module_harness = ["templates/builtin/*.json"]
|
|
45
|
+
|
|
46
|
+
[tool.pytest.ini_options]
|
|
47
|
+
# smoke = 真实 LLM 冒烟(活 API 调用:慢、耗额度、非确定性 flake)。
|
|
48
|
+
# 默认全量排除;要看时 pytest -m smoke 显式 opt-in(CLI 的 -m 覆盖 addopts)。
|
|
49
|
+
markers = [
|
|
50
|
+
"smoke: 真实 LLM 冒烟测试(活 API 调用,非确定性;默认排除,pytest -m smoke 显式运行)",
|
|
51
|
+
]
|
|
52
|
+
addopts = "-m 'not smoke'"
|
|
@@ -8,6 +8,7 @@ llm/mock.py
|
|
|
8
8
|
module_harness/__init__.py
|
|
9
9
|
module_harness/align.py
|
|
10
10
|
module_harness/builtins.py
|
|
11
|
+
module_harness/call.py
|
|
11
12
|
module_harness/checkpoint.py
|
|
12
13
|
module_harness/cli.py
|
|
13
14
|
module_harness/command.py
|
|
@@ -29,6 +30,7 @@ module_harness/scaffold.py
|
|
|
29
30
|
module_harness/spec.py
|
|
30
31
|
module_harness/status.py
|
|
31
32
|
module_harness/store.py
|
|
33
|
+
module_harness/stream.py
|
|
32
34
|
module_harness/submodule.py
|
|
33
35
|
module_harness/translator.py
|
|
34
36
|
module_harness/templates/builtin/codereview.json
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|