ltcai 9.9.2 → 9.9.3
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/README.md +39 -39
- package/docs/CHANGELOG.md +44 -0
- package/docs/COMMUNITY_AND_PLUGINS.md +1 -1
- package/docs/DEVELOPMENT.md +1 -1
- package/docs/ONBOARDING.md +1 -1
- package/docs/OPERATIONS.md +1 -1
- package/docs/TRUST_MODEL.md +1 -1
- package/docs/WHY_LATTICE.md +1 -1
- package/docs/kg-schema.md +1 -1
- package/lattice_brain/__init__.py +1 -1
- package/lattice_brain/graph/curator.py +141 -0
- package/lattice_brain/graph/documents.py +111 -0
- package/lattice_brain/graph/fusion.py +179 -0
- package/lattice_brain/graph/projection.py +161 -0
- package/lattice_brain/graph/retrieval.py +19 -1
- package/lattice_brain/ingestion.py +66 -0
- package/lattice_brain/runtime/multi_agent.py +1 -1
- package/latticeai/__init__.py +1 -1
- package/latticeai/api/automation_intelligence.py +159 -0
- package/latticeai/api/browser.py +15 -4
- package/latticeai/api/chat.py +27 -2
- package/latticeai/api/chat_agent_http.py +191 -3
- package/latticeai/api/chat_contracts.py +7 -1
- package/latticeai/api/chat_helpers.py +147 -0
- package/latticeai/api/chat_intents.py +189 -1
- package/latticeai/api/chat_stream.py +17 -1
- package/latticeai/api/funnel_metrics.py +31 -0
- package/latticeai/api/knowledge_graph.py +30 -0
- package/latticeai/api/local_files.py +90 -0
- package/latticeai/api/setup.py +134 -1
- package/latticeai/api/tools.py +33 -3
- package/latticeai/app_factory.py +14 -0
- package/latticeai/core/agent.py +91 -9
- package/latticeai/core/agent_eval.py +107 -0
- package/latticeai/core/file_generation.py +279 -2
- package/latticeai/core/legacy_compatibility.py +1 -1
- package/latticeai/core/marketplace.py +1 -1
- package/latticeai/core/workspace_os.py +1 -1
- package/latticeai/runtime/persistence_runtime.py +19 -1
- package/latticeai/runtime/router_registration.py +8 -1
- package/latticeai/services/app_context.py +3 -0
- package/latticeai/services/architecture_readiness.py +1 -1
- package/latticeai/services/automation_execution.py +266 -0
- package/latticeai/services/automation_intelligence.py +7 -0
- package/latticeai/services/command_center.py +20 -1
- package/latticeai/services/folder_watch.py +391 -0
- package/latticeai/services/funnel_metrics.py +202 -0
- package/latticeai/services/product_readiness.py +1 -1
- package/latticeai/services/search_service.py +17 -1
- package/latticeai/setup/demo_corpus.py +98 -0
- package/latticeai/tools/filesystem.py +38 -1
- package/package.json +1 -1
- package/scripts/bench_models.py +234 -5
- package/scripts/check_current_release_docs.mjs +1 -1
- package/src-tauri/Cargo.lock +1 -1
- package/src-tauri/Cargo.toml +1 -1
- package/src-tauri/tauri.conf.json +1 -1
- package/static/app/asset-manifest.json +28 -27
- package/static/app/assets/Act-DmdruVKV.js +1 -0
- package/static/app/assets/{Brain-BnGbytHa.js → Brain-DEY9jLVt.js} +2 -2
- package/static/app/assets/BrainHome-CeNaxjP1.js +2 -0
- package/static/app/assets/BrainSignals-CStjIqYi.js +1 -0
- package/static/app/assets/{Capture-CAKnLcb0.js → Capture-CVZ09QXi.js} +1 -1
- package/static/app/assets/CommandPalette-DepwOQFv.js +1 -0
- package/static/app/assets/Library-Bp0n-HlW.js +1 -0
- package/static/app/assets/{LivingBrain-C3VbogVo.js → LivingBrain-DxP4efJF.js} +1 -1
- package/static/app/assets/{ProductFlow-BLau7c7N.js → ProductFlow-DRbm7NEq.js} +1 -1
- package/static/app/assets/ReviewCard-C4HAO7A3.js +3 -0
- package/static/app/assets/{System-DRHc_6Su.js → System-ByQcmJW-.js} +1 -1
- package/static/app/assets/{bot-DZkDdh-A.js → bot-BNDyZLR7.js} +1 -1
- package/static/app/assets/circle-play-BkhdcHgd.js +1 -0
- package/static/app/assets/{cpu-DyvsDBbj.js → cpu-C6jjYm6i.js} +1 -1
- package/static/app/assets/{folder-open-CffoIr9t.js → folder-open-DjGIvDBQ.js} +1 -1
- package/static/app/assets/{hard-drive-C1oojrHb.js → hard-drive-BlSbwSaT.js} +1 -1
- package/static/app/assets/index-Bge3DXW7.css +2 -0
- package/static/app/assets/{index-BS_3aN1n.js → index-CHu7cgj3.js} +3 -3
- package/static/app/assets/{input-B_U7zvbR.js → input-DVDI0YR3.js} +1 -1
- package/static/app/assets/{navigation-xG-gyjbe.js → navigation-BddhEWA0.js} +1 -1
- package/static/app/assets/{network-BF13HJJF.js → network-pYQt5oBu.js} +1 -1
- package/static/app/assets/{primitives-DsuKh3M8.js → primitives-D7gCdEvS.js} +1 -1
- package/static/app/assets/shield-alert-K9RKGQeg.js +1 -0
- package/static/app/assets/{textarea-DsDL4ET8.js → textarea-sqQmoBKL.js} +1 -1
- package/static/app/assets/useFocusTrap-7EV9dFP2.js +1 -0
- package/static/app/assets/utils-uQYKXNeq.js +7 -0
- package/static/app/index.html +4 -4
- package/static/sw.js +1 -1
- package/static/app/assets/Act-Cx_gZNbE.js +0 -1
- package/static/app/assets/BrainHome-CRUMJL93.js +0 -2
- package/static/app/assets/BrainSignals-BP8-Qh2g.js +0 -1
- package/static/app/assets/CommandPalette-BoUTXBfm.js +0 -1
- package/static/app/assets/Library-PcfVUco6.js +0 -1
- package/static/app/assets/ReviewCard-CX5RqRqx.js +0 -1
- package/static/app/assets/download-C7yxYHvz.js +0 -1
- package/static/app/assets/index-DU78Kogz.css +0 -2
- package/static/app/assets/sunrise-CIP2ELJ1.js +0 -1
- package/static/app/assets/utils-Dh-Dwtkb.js +0 -7
package/latticeai/app_factory.py
CHANGED
|
@@ -395,6 +395,7 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
|
|
|
395
395
|
INGESTION_PIPELINE = _persistence_runtime["INGESTION_PIPELINE"]
|
|
396
396
|
DEVICE_IDENTITY = _persistence_runtime["DEVICE_IDENTITY"]
|
|
397
397
|
KG_PORTABILITY = _persistence_runtime["KG_PORTABILITY"]
|
|
398
|
+
FUNNEL_METRICS = _persistence_runtime["FUNNEL_METRICS"]
|
|
398
399
|
|
|
399
400
|
def _require_graph():
|
|
400
401
|
if not ENABLE_GRAPH or KNOWLEDGE_GRAPH is None:
|
|
@@ -951,6 +952,7 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
|
|
|
951
952
|
public_model=PUBLIC_MODEL,
|
|
952
953
|
local_model=LOCAL_MODEL or "",
|
|
953
954
|
on_chat_message=on_chat_message,
|
|
955
|
+
funnel_metrics=FUNNEL_METRICS,
|
|
954
956
|
)
|
|
955
957
|
app.state.context = context
|
|
956
958
|
|
|
@@ -1036,6 +1038,17 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
|
|
|
1036
1038
|
gate_write=PLATFORM.gate_write,
|
|
1037
1039
|
append_audit_event=append_audit_event,
|
|
1038
1040
|
workspace_graph=_workspace_graph,
|
|
1041
|
+
run_executor=RUN_EXECUTOR,
|
|
1042
|
+
review_queue=REVIEW_QUEUE,
|
|
1043
|
+
)
|
|
1044
|
+
)
|
|
1045
|
+
# UX funnel metrics (backlog #16): admin-only runtime counters.
|
|
1046
|
+
from latticeai.api.funnel_metrics import create_funnel_metrics_router
|
|
1047
|
+
|
|
1048
|
+
app.include_router(
|
|
1049
|
+
create_funnel_metrics_router(
|
|
1050
|
+
service=FUNNEL_METRICS,
|
|
1051
|
+
require_admin=require_admin,
|
|
1039
1052
|
)
|
|
1040
1053
|
)
|
|
1041
1054
|
COMMAND_CENTER = CommandCenterService(
|
|
@@ -1212,6 +1225,7 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
|
|
|
1212
1225
|
gardener=gardener,
|
|
1213
1226
|
create_setup_router=create_setup_router,
|
|
1214
1227
|
model_router=router,
|
|
1228
|
+
knowledge_graph=KNOWLEDGE_GRAPH,
|
|
1215
1229
|
)
|
|
1216
1230
|
|
|
1217
1231
|
# ── Entry Point ────────────────────────────────────────────────────────────────
|
package/latticeai/core/agent.py
CHANGED
|
@@ -27,7 +27,7 @@ import re
|
|
|
27
27
|
from dataclasses import dataclass
|
|
28
28
|
from enum import Enum
|
|
29
29
|
from pathlib import Path
|
|
30
|
-
from typing import Any, Awaitable, Callable, Dict, FrozenSet, List, Optional, Tuple
|
|
30
|
+
from typing import Any, Awaitable, Callable, Dict, FrozenSet, List, Mapping, Optional, Tuple
|
|
31
31
|
|
|
32
32
|
from lattice_brain.runtime.hooks import dispatch_tool
|
|
33
33
|
from lattice_brain.runtime.contracts import runtime_boundary_contract, single_agent_contract
|
|
@@ -227,6 +227,44 @@ def filter_learnings(learnings: List[Any]) -> List[str]:
|
|
|
227
227
|
return kept
|
|
228
228
|
|
|
229
229
|
|
|
230
|
+
@dataclass(frozen=True)
|
|
231
|
+
class PhaseBudgets:
|
|
232
|
+
"""Per-phase token budgets for the agent loop.
|
|
233
|
+
|
|
234
|
+
One shared budget let a weak model burn everything on planning prose and
|
|
235
|
+
reach EXECUTE with nothing left. Each role phase now has its own cap, so
|
|
236
|
+
a verbose planner can never starve execution or verification. Defaults
|
|
237
|
+
match the historical hardcoded values; every cap is overridable through
|
|
238
|
+
the ``Config.from_env`` environment pattern.
|
|
239
|
+
"""
|
|
240
|
+
|
|
241
|
+
plan_tokens: int = 1024
|
|
242
|
+
execute_tokens: int = 4096
|
|
243
|
+
verify_tokens: int = 512
|
|
244
|
+
memory_tokens: int = 256
|
|
245
|
+
|
|
246
|
+
@classmethod
|
|
247
|
+
def from_env(cls, env: Optional[Mapping[str, str]] = None) -> "PhaseBudgets":
|
|
248
|
+
from latticeai.core.config import _int
|
|
249
|
+
|
|
250
|
+
if env is None:
|
|
251
|
+
import os
|
|
252
|
+
|
|
253
|
+
env = os.environ
|
|
254
|
+
|
|
255
|
+
def cap(key: str, default: int) -> int:
|
|
256
|
+
# A misconfigured/absurd value must not brick the loop: floor at a
|
|
257
|
+
# budget that still fits one JSON action object.
|
|
258
|
+
return max(128, _int(env, key, default))
|
|
259
|
+
|
|
260
|
+
return cls(
|
|
261
|
+
plan_tokens=cap("LATTICEAI_AGENT_PLAN_TOKENS", cls.plan_tokens),
|
|
262
|
+
execute_tokens=cap("LATTICEAI_AGENT_EXECUTE_TOKENS", cls.execute_tokens),
|
|
263
|
+
verify_tokens=cap("LATTICEAI_AGENT_VERIFY_TOKENS", cls.verify_tokens),
|
|
264
|
+
memory_tokens=cap("LATTICEAI_AGENT_MEMORY_TOKENS", cls.memory_tokens),
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
|
|
230
268
|
@dataclass
|
|
231
269
|
class AgentDeps:
|
|
232
270
|
"""The ports a :class:`SingleAgentRuntime` needs from the outside world.
|
|
@@ -286,12 +324,29 @@ class AgentDeps:
|
|
|
286
324
|
# returning None (fall through to the classic gates) or a verdict dict.
|
|
287
325
|
change_governor: Any = None
|
|
288
326
|
|
|
327
|
+
# ── phase budgets (optional) ─────────────────────────────────────
|
|
328
|
+
# Per-phase token caps (plan/execute/verify/memory). None reads the
|
|
329
|
+
# environment once at first use; tests inject a fixed PhaseBudgets.
|
|
330
|
+
phase_budgets: Optional[PhaseBudgets] = None
|
|
331
|
+
|
|
289
332
|
|
|
290
333
|
class SingleAgentRuntime:
|
|
291
334
|
"""Drives the agent state machine over injected :class:`AgentDeps`."""
|
|
292
335
|
|
|
293
336
|
def __init__(self, deps: AgentDeps) -> None:
|
|
294
337
|
self.deps = deps
|
|
338
|
+
self._env_phase_budgets: Optional[PhaseBudgets] = None
|
|
339
|
+
|
|
340
|
+
@property
|
|
341
|
+
def phase_budgets(self) -> PhaseBudgets:
|
|
342
|
+
# getattr twice: partially-constructed runtimes/deps (tests build them
|
|
343
|
+
# via __new__ or minimal fakes) still get working default budgets.
|
|
344
|
+
injected = getattr(self.deps, "phase_budgets", None)
|
|
345
|
+
if injected is not None:
|
|
346
|
+
return injected
|
|
347
|
+
if getattr(self, "_env_phase_budgets", None) is None:
|
|
348
|
+
self._env_phase_budgets = PhaseBudgets.from_env()
|
|
349
|
+
return self._env_phase_budgets
|
|
295
350
|
|
|
296
351
|
def boundary(self) -> Dict[str, Any]:
|
|
297
352
|
return runtime_boundary_contract(
|
|
@@ -331,7 +386,7 @@ class SingleAgentRuntime:
|
|
|
331
386
|
raw = await d.generate_as(
|
|
332
387
|
model_id,
|
|
333
388
|
message="Produce a JSON execution plan for this request.",
|
|
334
|
-
context=context, max_tokens=
|
|
389
|
+
context=context, max_tokens=self.phase_budgets.plan_tokens, temperature=0.1,
|
|
335
390
|
)
|
|
336
391
|
ctx.trace.llm_call("plan", model=model_id)
|
|
337
392
|
try:
|
|
@@ -360,8 +415,14 @@ class SingleAgentRuntime:
|
|
|
360
415
|
ctx.state = AgentState.WAITING_APPROVAL
|
|
361
416
|
|
|
362
417
|
# ── APPROVAL ─────────────────────────────────────────────────────
|
|
363
|
-
def
|
|
364
|
-
"""
|
|
418
|
+
def approval_requirements(self, ctx: AgentRunContext) -> Dict[str, Any]:
|
|
419
|
+
"""Read-only preview of the approval gate for a planned run.
|
|
420
|
+
|
|
421
|
+
Shares the exact predicate :meth:`approve` enforces, so the HTTP
|
|
422
|
+
layer can pause a run as ``awaiting_approval`` (with a plan summary
|
|
423
|
+
for the user) instead of letting it fail closed — without ever
|
|
424
|
+
weakening the gate itself.
|
|
425
|
+
"""
|
|
365
426
|
d = self.deps
|
|
366
427
|
auto_approve_tools = {name for name, p in d.tool_governance.items() if p["auto_approve"]}
|
|
367
428
|
# Governor-managed tools never hard-block the plan: each call is
|
|
@@ -377,7 +438,26 @@ class SingleAgentRuntime:
|
|
|
377
438
|
if s.get("action") not in auto_approve_tools
|
|
378
439
|
and s.get("action") not in governed_tools
|
|
379
440
|
]
|
|
380
|
-
requires = ctx.plan.get("requires_approval", False) or bool(non_auto)
|
|
441
|
+
requires = bool(ctx.plan.get("requires_approval", False)) or bool(non_auto)
|
|
442
|
+
lines = [
|
|
443
|
+
f"{index}. {step.get('description') or step.get('action') or '?'}"
|
|
444
|
+
for index, step in enumerate(steps, start=1)
|
|
445
|
+
]
|
|
446
|
+
summary = str(ctx.plan.get("goal") or "").strip()
|
|
447
|
+
if lines:
|
|
448
|
+
summary = (summary + "\n" if summary else "") + "\n".join(lines)
|
|
449
|
+
return {
|
|
450
|
+
"requires_approval": requires,
|
|
451
|
+
"non_auto_steps": non_auto,
|
|
452
|
+
"plan_summary": summary,
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
def approve(self, ctx: AgentRunContext, current_user: str, *, approved_by_human: bool = False) -> None:
|
|
456
|
+
"""APPROVAL: Check governance, log decision, auto-approve (future: UI prompt)."""
|
|
457
|
+
d = self.deps
|
|
458
|
+
requirements = self.approval_requirements(ctx)
|
|
459
|
+
non_auto = requirements["non_auto_steps"]
|
|
460
|
+
requires = requirements["requires_approval"]
|
|
381
461
|
|
|
382
462
|
ctx.transcript.append({
|
|
383
463
|
"state": AgentState.WAITING_APPROVAL.value,
|
|
@@ -420,7 +500,8 @@ class SingleAgentRuntime:
|
|
|
420
500
|
raw = await d.generate_as(
|
|
421
501
|
model_id,
|
|
422
502
|
message="Execute the next step.",
|
|
423
|
-
context=context, max_tokens=
|
|
503
|
+
context=context, max_tokens=self.phase_budgets.execute_tokens,
|
|
504
|
+
temperature=req.temperature,
|
|
424
505
|
)
|
|
425
506
|
ctx.trace.llm_call("execute", model=model_id)
|
|
426
507
|
try:
|
|
@@ -727,7 +808,7 @@ class SingleAgentRuntime:
|
|
|
727
808
|
raw = await d.generate_as(
|
|
728
809
|
model_id,
|
|
729
810
|
message="Review the execution transcript and return your verdict JSON.",
|
|
730
|
-
context=context, max_tokens=
|
|
811
|
+
context=context, max_tokens=self.phase_budgets.verify_tokens, temperature=0.1,
|
|
731
812
|
)
|
|
732
813
|
ctx.trace.llm_call("verify", model=model_id)
|
|
733
814
|
verdict: Optional[Dict[str, Any]] = None
|
|
@@ -749,7 +830,8 @@ class SingleAgentRuntime:
|
|
|
749
830
|
raw = await d.generate_as(
|
|
750
831
|
model_id,
|
|
751
832
|
message="Return your verdict as one strict JSON object.",
|
|
752
|
-
context=strict_context, max_tokens=
|
|
833
|
+
context=strict_context, max_tokens=self.phase_budgets.verify_tokens,
|
|
834
|
+
temperature=0.0,
|
|
753
835
|
)
|
|
754
836
|
ctx.trace.llm_call("verify", model=model_id)
|
|
755
837
|
try:
|
|
@@ -899,7 +981,7 @@ class SingleAgentRuntime:
|
|
|
899
981
|
try:
|
|
900
982
|
raw = await d.generate(
|
|
901
983
|
message="Extract learnings from this completed task.",
|
|
902
|
-
context=context, max_tokens=
|
|
984
|
+
context=context, max_tokens=self.phase_budgets.memory_tokens, temperature=0.1,
|
|
903
985
|
)
|
|
904
986
|
mem = extract_action(str(raw))
|
|
905
987
|
kept_learnings = filter_learnings(mem.get("learnings") or [])
|
|
@@ -163,6 +163,18 @@ class Scenario:
|
|
|
163
163
|
# canned results (node ids, snippets, proposal ids), so an answer that is
|
|
164
164
|
# not grounded in the retrieved/executed evidence fails the scenario.
|
|
165
165
|
expect_final_contains: List[str] = field(default_factory=list)
|
|
166
|
+
# Exact per-name counts against the LoopTrace ``repairs`` histogram —
|
|
167
|
+
# filegen scenarios use this to prove the ArtifactWritePipeline actually
|
|
168
|
+
# fired (``artifact_sanitize`` / ``artifact_repair``), not merely that
|
|
169
|
+
# the tool ran.
|
|
170
|
+
expect_repairs: Dict[str, int] = field(default_factory=dict)
|
|
171
|
+
# Content-level assertions on what file-writing tools actually received
|
|
172
|
+
# AFTER sanitize: every ``contains`` needle must appear in at least one
|
|
173
|
+
# written payload, every ``excludes`` needle in none. This is the teeth
|
|
174
|
+
# of the dirty-write scenarios — a fence or chat wrapper reaching the
|
|
175
|
+
# tool port fails the gate.
|
|
176
|
+
expect_write_contains: List[str] = field(default_factory=list)
|
|
177
|
+
expect_write_excludes: List[str] = field(default_factory=list)
|
|
166
178
|
max_steps: int = 8
|
|
167
179
|
|
|
168
180
|
|
|
@@ -247,6 +259,37 @@ _WRITE = '{"action": "write_file", "args": {"path": "note.txt", "content": "hi"}
|
|
|
247
259
|
_FINAL = '{"action": "final", "message": "done"}'
|
|
248
260
|
_PASS = '{"action": "verdict", "verdict": "PASS", "next_state": "DONE", "reason": "ok"}'
|
|
249
261
|
|
|
262
|
+
# ── dirty write_file payloads (ArtifactWritePipeline scenarios) ──────────
|
|
263
|
+
# What weak local models actually put in args.content: chat framing + a
|
|
264
|
+
# Markdown fence around the document. The JSON envelope itself is valid —
|
|
265
|
+
# the dirt is *inside* the content string, so only the write-side sanitize
|
|
266
|
+
# pass (agent.py _dispatch_step → sanitize_write_content) can clean it.
|
|
267
|
+
_DIRTY_HTML_CONTENT = (
|
|
268
|
+
"Sure! Here is your page:\\n"
|
|
269
|
+
"```html\\n"
|
|
270
|
+
"<!DOCTYPE html>\\n<html>\\n<head><meta charset=\\\"utf-8\\\">"
|
|
271
|
+
"<title>Hello</title></head>\\n<body><h1>Hello</h1></body>\\n</html>\\n"
|
|
272
|
+
"```\\n"
|
|
273
|
+
"Hope this helps! Let me know if you need changes."
|
|
274
|
+
)
|
|
275
|
+
_DIRTY_WRITE = (
|
|
276
|
+
'{"thoughts": "produce the page", "action": "write_file", '
|
|
277
|
+
'"args": {"path": "pages/hello.html", "content": "' + _DIRTY_HTML_CONTENT + '"}}'
|
|
278
|
+
)
|
|
279
|
+
# Token-limit casualty: the document just stops mid-body (no </body>/</html>).
|
|
280
|
+
_TRUNCATED_HTML_CONTENT = (
|
|
281
|
+
"<!DOCTYPE html>\\n<html>\\n<head><meta charset=\\\"utf-8\\\">"
|
|
282
|
+
"<title>Weekly</title></head>\\n<body>\\n<h1>Weekly report</h1>"
|
|
283
|
+
)
|
|
284
|
+
_TRUNCATED_WRITE = (
|
|
285
|
+
'{"thoughts": "write the report", "action": "write_file", '
|
|
286
|
+
'"args": {"path": "reports/weekly.html", "content": "' + _TRUNCATED_HTML_CONTENT + '"}}'
|
|
287
|
+
)
|
|
288
|
+
_FILEGEN_PLAN = (
|
|
289
|
+
'{"action": "plan", "goal": "save the requested web page to disk", '
|
|
290
|
+
'"steps": [{"action": "write_file"}]}'
|
|
291
|
+
)
|
|
292
|
+
|
|
250
293
|
|
|
251
294
|
def default_scenarios() -> List[Scenario]:
|
|
252
295
|
return [
|
|
@@ -551,6 +594,54 @@ def default_scenarios() -> List[Scenario]:
|
|
|
551
594
|
expected_class="failed",
|
|
552
595
|
expect_tool_outcomes={"error": 1},
|
|
553
596
|
),
|
|
597
|
+
# ── ArtifactWritePipeline (write-side sanitize) ─────────────────
|
|
598
|
+
Scenario(
|
|
599
|
+
name="filegen-dirty-write-sanitized-critic-pass",
|
|
600
|
+
# Weak model wraps args.content in prose + a Markdown fence. The
|
|
601
|
+
# sanitize pass must strip the wrapper BEFORE the tool runs, the
|
|
602
|
+
# trace must record artifact_sanitize, and the critic PASS then
|
|
603
|
+
# completes over a transcript whose written payload is clean.
|
|
604
|
+
replies=[_FILEGEN_PLAN, _DIRTY_WRITE, _FINAL, _PASS],
|
|
605
|
+
expect_exact={"parse_errors": 0},
|
|
606
|
+
expect_tool_outcomes={"ok": 1},
|
|
607
|
+
expect_tool_calls=["write_file"],
|
|
608
|
+
expect_repairs={"artifact_sanitize": 1},
|
|
609
|
+
expect_write_contains=["<!DOCTYPE html>", "</html>"],
|
|
610
|
+
expect_write_excludes=["```", "Sure!", "Hope this helps"],
|
|
611
|
+
),
|
|
612
|
+
Scenario(
|
|
613
|
+
name="filegen-truncated-write-repaired-critic-pass",
|
|
614
|
+
# Token-limit truncation: no extractable clean document exists, so
|
|
615
|
+
# the pipeline's deterministic repair must close the document
|
|
616
|
+
# (artifact_repair) — the file on disk is still structurally valid.
|
|
617
|
+
replies=[_FILEGEN_PLAN, _TRUNCATED_WRITE, _FINAL, _PASS],
|
|
618
|
+
expect_exact={"parse_errors": 0},
|
|
619
|
+
expect_tool_outcomes={"ok": 1},
|
|
620
|
+
expect_tool_calls=["write_file"],
|
|
621
|
+
expect_repairs={"artifact_repair": 1},
|
|
622
|
+
expect_write_contains=["<h1>Weekly report</h1>", "</body>", "</html>"],
|
|
623
|
+
expect_write_excludes=["```"],
|
|
624
|
+
),
|
|
625
|
+
Scenario(
|
|
626
|
+
name="filegen-dirty-write-unverifiable-needs-review",
|
|
627
|
+
# The dirty write is sanitized and runs, but the critic never
|
|
628
|
+
# produces a parseable verdict — fail-closed: the run must end
|
|
629
|
+
# NEEDS_REVIEW (never a fabricated DONE), while the written file
|
|
630
|
+
# stays clean because sanitize ran before the tool.
|
|
631
|
+
replies=[
|
|
632
|
+
_FILEGEN_PLAN, _DIRTY_WRITE, _FINAL,
|
|
633
|
+
"the page looks great, approving!", # unparseable critic
|
|
634
|
+
"still prose, still not a JSON verdict", # strict retry fails too
|
|
635
|
+
],
|
|
636
|
+
expect_state="NEEDS_REVIEW",
|
|
637
|
+
expected_class="needs_review",
|
|
638
|
+
expect_min={"parse_errors": 2},
|
|
639
|
+
expect_tool_outcomes={"ok": 1},
|
|
640
|
+
expect_tool_calls=["write_file"],
|
|
641
|
+
expect_repairs={"artifact_sanitize": 1},
|
|
642
|
+
expect_write_excludes=["```", "Sure!"],
|
|
643
|
+
expect_final_contains=["직접 확인"],
|
|
644
|
+
),
|
|
554
645
|
]
|
|
555
646
|
|
|
556
647
|
|
|
@@ -591,6 +682,22 @@ async def _run_scenario(scenario: Scenario) -> Dict[str, Any]:
|
|
|
591
682
|
for needle in scenario.expect_final_contains:
|
|
592
683
|
if needle not in ctx.final_message:
|
|
593
684
|
failures.append(f"final_message missing {needle!r}")
|
|
685
|
+
for repair_name, count in scenario.expect_repairs.items():
|
|
686
|
+
seen = int((summary.get("repairs") or {}).get(repair_name, 0))
|
|
687
|
+
if seen != count:
|
|
688
|
+
failures.append(f"repairs[{repair_name}]={seen} != {count}")
|
|
689
|
+
# Payloads the file-writing tools actually received (post-sanitize).
|
|
690
|
+
written = [
|
|
691
|
+
str((call.get("args") or {}).get("content") or "")
|
|
692
|
+
for call in tool_log
|
|
693
|
+
if call["name"] in ("write_file", "generate_file")
|
|
694
|
+
]
|
|
695
|
+
for needle in scenario.expect_write_contains:
|
|
696
|
+
if not any(needle in payload for payload in written):
|
|
697
|
+
failures.append(f"written content missing {needle!r}")
|
|
698
|
+
for needle in scenario.expect_write_excludes:
|
|
699
|
+
if any(needle in payload for payload in written):
|
|
700
|
+
failures.append(f"written content must not contain {needle!r}")
|
|
594
701
|
if governor is not None and summary["tool_outcomes"].get("proposed", 0) != len(governor.proposals):
|
|
595
702
|
failures.append(
|
|
596
703
|
f"governor proposals={len(governor.proposals)} != "
|