forge-agent 1.1.5__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.
Files changed (129) hide show
  1. forge/__init__.py +63 -0
  2. forge/brain/__init__.py +4 -0
  3. forge/brain/approval.py +111 -0
  4. forge/brain/composer.py +54 -0
  5. forge/brain/contracts.py +128 -0
  6. forge/brain/council.py +244 -0
  7. forge/brain/intent.py +331 -0
  8. forge/brain/mission_store.py +186 -0
  9. forge/brain/operator.py +716 -0
  10. forge/brain/orchestrator.py +1541 -0
  11. forge/brain/planner.py +992 -0
  12. forge/brain/prompt.py +62 -0
  13. forge/brain/worker_executor.py +139 -0
  14. forge/brain/worker_protocol.py +56 -0
  15. forge/brain/worker_runtime.py +395 -0
  16. forge/cli/__init__.py +0 -0
  17. forge/cli/main.py +561 -0
  18. forge/config/__init__.py +3 -0
  19. forge/config/settings.py +63 -0
  20. forge/core/__init__.py +0 -0
  21. forge/core/discovery.py +291 -0
  22. forge/core/models.py +164 -0
  23. forge/core/quota.py +252 -0
  24. forge/core/router.py +560 -0
  25. forge/core/session.py +309 -0
  26. forge/desktop/__init__.py +2 -0
  27. forge/desktop/account_client.py +149 -0
  28. forge/desktop/app.py +371 -0
  29. forge/desktop/diagnostics.py +23 -0
  30. forge/desktop/runtime.py +1447 -0
  31. forge/desktop/server.py +3261 -0
  32. forge/memory/__init__.py +4 -0
  33. forge/memory/context.py +65 -0
  34. forge/memory/graph.py +351 -0
  35. forge/providers/__init__.py +8 -0
  36. forge/providers/anthropic.py +128 -0
  37. forge/providers/base.py +324 -0
  38. forge/providers/cloudflare.py +173 -0
  39. forge/providers/deepseek.py +129 -0
  40. forge/providers/gemini.py +135 -0
  41. forge/providers/groq.py +159 -0
  42. forge/providers/mistral.py +133 -0
  43. forge/providers/nvidia.py +145 -0
  44. forge/providers/ollama.py +264 -0
  45. forge/providers/openai.py +164 -0
  46. forge/providers/openrouter.py +139 -0
  47. forge/providers/registry.py +44 -0
  48. forge/providers/together.py +141 -0
  49. forge/recovery/__init__.py +3 -0
  50. forge/recovery/manager.py +47 -0
  51. forge/runtime/__init__.py +28 -0
  52. forge/runtime/agent.py +200 -0
  53. forge/runtime/contracts.py +59 -0
  54. forge/runtime/gateway.py +190 -0
  55. forge/runtime/heartbeat.py +108 -0
  56. forge/runtime/lanes.py +129 -0
  57. forge/runtime/markdown_memory.py +265 -0
  58. forge/runtime/state_store.py +877 -0
  59. forge/runtime/worker_host.py +207 -0
  60. forge/safety/__init__.py +3 -0
  61. forge/safety/guard.py +156 -0
  62. forge/safety/sanitizer.py +78 -0
  63. forge/skills/__init__.py +5 -0
  64. forge/skills/contracts.py +69 -0
  65. forge/skills/loader.py +129 -0
  66. forge/skills/registry.py +294 -0
  67. forge/skills/router.py +391 -0
  68. forge/skills/runtime.py +86 -0
  69. forge/skills_catalog/_template/SKILL.md +50 -0
  70. forge/skills_catalog/affiliate-offer-analyzer/SKILL.md +54 -0
  71. forge/skills_catalog/affiliate-offer-analyzer/executor.py +26 -0
  72. forge/skills_catalog/affiliate-offer-analyzer/schema.json +69 -0
  73. forge/skills_catalog/artifact-writer/SKILL.md +55 -0
  74. forge/skills_catalog/artifact-writer/executor.py +74 -0
  75. forge/skills_catalog/artifact-writer/schema.json +67 -0
  76. forge/skills_catalog/browser-executor/SKILL.md +65 -0
  77. forge/skills_catalog/browser-executor/executor.py +143 -0
  78. forge/skills_catalog/browser-executor/schema.json +133 -0
  79. forge/skills_catalog/codebase-analyzer/SKILL.md +64 -0
  80. forge/skills_catalog/codebase-analyzer/executor.py +230 -0
  81. forge/skills_catalog/codebase-analyzer/schema.json +67 -0
  82. forge/skills_catalog/external-publisher/SKILL.md +63 -0
  83. forge/skills_catalog/external-publisher/executor.py +54 -0
  84. forge/skills_catalog/external-publisher/schema.json +90 -0
  85. forge/skills_catalog/file-editor/SKILL.md +68 -0
  86. forge/skills_catalog/file-editor/executor.py +223 -0
  87. forge/skills_catalog/file-editor/schema.json +106 -0
  88. forge/skills_catalog/file-reader/SKILL.md +60 -0
  89. forge/skills_catalog/file-reader/executor.py +85 -0
  90. forge/skills_catalog/file-reader/schema.json +61 -0
  91. forge/skills_catalog/github-publisher/SKILL.md +71 -0
  92. forge/skills_catalog/github-publisher/executor.py +65 -0
  93. forge/skills_catalog/github-publisher/schema.json +97 -0
  94. forge/skills_catalog/research-brief/SKILL.md +54 -0
  95. forge/skills_catalog/research-brief/executor.py +23 -0
  96. forge/skills_catalog/research-brief/schema.json +68 -0
  97. forge/skills_catalog/seo-article-writer/SKILL.md +54 -0
  98. forge/skills_catalog/seo-article-writer/executor.py +24 -0
  99. forge/skills_catalog/seo-article-writer/schema.json +64 -0
  100. forge/skills_catalog/shell-executor/SKILL.md +65 -0
  101. forge/skills_catalog/shell-executor/executor.py +80 -0
  102. forge/skills_catalog/shell-executor/schema.json +84 -0
  103. forge/skills_catalog/system-inspector/SKILL.md +62 -0
  104. forge/skills_catalog/system-inspector/executor.py +93 -0
  105. forge/skills_catalog/system-inspector/schema.json +83 -0
  106. forge/skills_catalog/wordpress-publisher/SKILL.md +73 -0
  107. forge/skills_catalog/wordpress-publisher/executor.py +68 -0
  108. forge/skills_catalog/wordpress-publisher/schema.json +101 -0
  109. forge/skills_catalog/workspace-inspector/SKILL.md +55 -0
  110. forge/skills_catalog/workspace-inspector/executor.py +24 -0
  111. forge/skills_catalog/workspace-inspector/schema.json +72 -0
  112. forge/tools/__init__.py +15 -0
  113. forge/tools/browser.py +600 -0
  114. forge/tools/credentials.py +70 -0
  115. forge/tools/github.py +190 -0
  116. forge/tools/publish.py +50 -0
  117. forge/tools/shell.py +152 -0
  118. forge/tools/system.py +219 -0
  119. forge/tools/wordpress.py +159 -0
  120. forge/tools/workspace.py +422 -0
  121. forge/validation/__init__.py +11 -0
  122. forge/validation/json_validator.py +64 -0
  123. forge/validation/validator.py +273 -0
  124. forge_agent-1.1.5.dist-info/METADATA +255 -0
  125. forge_agent-1.1.5.dist-info/RECORD +129 -0
  126. forge_agent-1.1.5.dist-info/WHEEL +5 -0
  127. forge_agent-1.1.5.dist-info/entry_points.txt +2 -0
  128. forge_agent-1.1.5.dist-info/licenses/LICENSE +21 -0
  129. forge_agent-1.1.5.dist-info/top_level.txt +1 -0
forge/__init__.py ADDED
@@ -0,0 +1,63 @@
1
+ """
2
+ FORGE — Free Open Reasoning & Generation Engine
3
+ ================================================
4
+ The world's most powerful free AI agent.
5
+ Self-evolving. Zero cost. Forever.
6
+
7
+ forge.ask("write me a web scraper") # auto-selects best free model
8
+ forge.code("fix this bug", file="x.py") # routes to coding specialist
9
+ forge.research("quantum computing") # deep multi-model research
10
+
11
+ GitHub : https://github.com/trenstudio/forge
12
+ Website: https://www.trenstudio.com/FORGE
13
+ License: MIT
14
+ """
15
+
16
+ __version__ = "1.1.5"
17
+ __author__ = "TREN Studio"
18
+ __license__ = "MIT"
19
+
20
+ from forge.core.router import ForgeRouter
21
+ from forge.core.session import ForgeSession
22
+ from forge.brain.operator import ForgeOperator
23
+ from forge.memory.graph import MemoryGraph
24
+ from forge.runtime import ForgeAgentRuntime
25
+
26
+ # One-line convenience API
27
+ _default_session: ForgeSession | None = None
28
+
29
+ def _session() -> ForgeSession:
30
+ global _default_session
31
+ if _default_session is None:
32
+ _default_session = ForgeSession()
33
+ return _default_session
34
+
35
+ def ask(prompt: str, **kwargs) -> str:
36
+ """Send a prompt. FORGE picks the best free model automatically."""
37
+ return _session().ask(prompt, **kwargs)
38
+
39
+ def code(prompt: str, **kwargs) -> str:
40
+ """Coding task — routed to the strongest available coding model."""
41
+ return _session().ask(prompt, task_type="code", **kwargs)
42
+
43
+ def research(prompt: str, **kwargs) -> str:
44
+ """Deep research task with optional web search."""
45
+ return _session().ask(prompt, task_type="research", **kwargs)
46
+
47
+ def operate(prompt: str, **kwargs) -> str:
48
+ """Run the skill-based operator brain."""
49
+ operator = ForgeOperator()
50
+ return operator.handle_as_text(prompt, **kwargs)
51
+
52
+ __all__ = [
53
+ "__version__",
54
+ "ForgeRouter",
55
+ "ForgeSession",
56
+ "ForgeOperator",
57
+ "ForgeAgentRuntime",
58
+ "MemoryGraph",
59
+ "ask",
60
+ "code",
61
+ "operate",
62
+ "research",
63
+ ]
@@ -0,0 +1,4 @@
1
+ from forge.brain.prompt import CORE_BRAIN_PROMPT
2
+ from forge.brain.operator import ForgeOperator
3
+
4
+ __all__ = ["CORE_BRAIN_PROMPT", "ForgeOperator"]
@@ -0,0 +1,111 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Any
5
+
6
+ from forge.runtime.state_store import PersistentStateStore
7
+
8
+
9
+ AUTO_APPROVE_CLASSES = {"local_readonly"}
10
+ HUMAN_APPROVAL_CLASSES = {
11
+ "network_post",
12
+ "network_egress",
13
+ "authenticated_browser",
14
+ "external_publish",
15
+ "sensitive_file_write",
16
+ "destructive_shell",
17
+ }
18
+
19
+
20
+ @dataclass(slots=True)
21
+ class ApprovalDecision:
22
+ allowed: bool
23
+ approval_required: bool
24
+ approval_id: str | None
25
+ approval_class: str | None
26
+ notes: list[str]
27
+ status: str
28
+
29
+
30
+ class ApprovalPolicyEngine:
31
+ """Policy + encrypted payload approval flow."""
32
+
33
+ def __init__(self, store: PersistentStateStore) -> None:
34
+ self._store = store
35
+
36
+ def evaluate(
37
+ self,
38
+ *,
39
+ mission_id: str,
40
+ step_id: str,
41
+ approval_class: str | None,
42
+ request_excerpt: str,
43
+ payload: dict[str, Any],
44
+ summary: str,
45
+ confirmed: bool,
46
+ ) -> ApprovalDecision:
47
+ if not approval_class:
48
+ return ApprovalDecision(True, False, None, None, [], "allowed")
49
+
50
+ if approval_class in AUTO_APPROVE_CLASSES:
51
+ return ApprovalDecision(
52
+ True,
53
+ False,
54
+ None,
55
+ approval_class,
56
+ [f"Approval class `{approval_class}` auto-approved by policy."],
57
+ "auto_approved",
58
+ )
59
+
60
+ if confirmed:
61
+ return ApprovalDecision(
62
+ True,
63
+ False,
64
+ None,
65
+ approval_class,
66
+ [f"Approval class `{approval_class}` approved explicitly by operator input."],
67
+ "confirmed",
68
+ )
69
+
70
+ if approval_class in HUMAN_APPROVAL_CLASSES:
71
+ approval_id = self._store.create_pending_approval(
72
+ mission_id=mission_id,
73
+ step_id=step_id,
74
+ approval_class=approval_class,
75
+ request_excerpt=request_excerpt[:500],
76
+ payload=payload,
77
+ summary=summary,
78
+ policy_mode="human_approval",
79
+ )
80
+ return ApprovalDecision(
81
+ False,
82
+ True,
83
+ approval_id,
84
+ approval_class,
85
+ [
86
+ f"Approval class: {approval_class}. Human confirmation is required before execution.",
87
+ f"Approval request created: {approval_id}.",
88
+ ],
89
+ "pending",
90
+ )
91
+
92
+ return ApprovalDecision(
93
+ True,
94
+ False,
95
+ None,
96
+ approval_class,
97
+ [f"Approval class `{approval_class}` allowed by current policy."],
98
+ "allowed",
99
+ )
100
+
101
+ def approval_status(self, approval_id: str) -> dict[str, Any] | None:
102
+ return self._store.get_approval(approval_id, include_payload=False)
103
+
104
+ def approve(self, approval_id: str, *, notes: str = "") -> dict[str, Any] | None:
105
+ return self._store.decide_approval(approval_id, approved=True, notes=notes)
106
+
107
+ def reject(self, approval_id: str, *, notes: str = "") -> dict[str, Any] | None:
108
+ return self._store.decide_approval(approval_id, approved=False, notes=notes)
109
+
110
+ def list_pending(self) -> list[dict[str, Any]]:
111
+ return self._store.list_approvals(status="pending")
@@ -0,0 +1,54 @@
1
+ from __future__ import annotations
2
+
3
+ from forge.brain.contracts import CompletionState, IntentKind, OperatorResult
4
+
5
+
6
+ class ResponseComposer:
7
+ """Compose the final operator-grade response."""
8
+
9
+ def compose(self, result: OperatorResult) -> str:
10
+ if not result.step_results and result.intent.primary_intent.value == "conversation":
11
+ return result.result.strip()
12
+
13
+ if not result.step_results and result.validation_status == CompletionState.FINISHED:
14
+ return result.result.strip()
15
+
16
+ if (
17
+ result.validation_status == CompletionState.FINISHED
18
+ and result.intent.primary_intent in {IntentKind.RESEARCH, IntentKind.ANALYSIS}
19
+ and not result.risks_or_limitations
20
+ ):
21
+ return result.result.strip()
22
+
23
+ validation = result.validation_status.value
24
+ risks = result.risks_or_limitations or []
25
+ completed = sum(1 for step in result.step_results if step.status == CompletionState.FINISHED)
26
+ total = len(result.step_results)
27
+
28
+ lines = [result.result.strip()]
29
+ if total:
30
+ lines.extend(
31
+ [
32
+ "",
33
+ f"Status: {validation}. Steps completed: {completed}/{total}.",
34
+ ]
35
+ )
36
+ else:
37
+ lines.extend(["", f"Status: {validation}."])
38
+ if risks:
39
+ lines.extend(["", "Limitations:"])
40
+ lines.extend(f"- {item}" for item in risks[:4])
41
+ if result.best_next_action:
42
+ lines.extend(["", f"Next: {result.best_next_action}"])
43
+ return "\n".join(line for line in lines if line is not None)
44
+
45
+ def best_next_action(self, status: CompletionState) -> str:
46
+ if status == CompletionState.FINISHED:
47
+ return "Proceed to the next business action or persist the artifact."
48
+ if status == CompletionState.PARTIALLY_FINISHED:
49
+ return "Review the partial output, then rerun the blocked step with tighter inputs."
50
+ if status == CompletionState.NEEDS_HUMAN_CONFIRMATION:
51
+ return "Review the risk note and provide explicit confirmation before execution."
52
+ if status == CompletionState.NEEDS_RETRY:
53
+ return "Retry the failed step or switch to the suggested fallback skill."
54
+ return "Inspect the failure reason, adjust inputs, and rerun safely."
@@ -0,0 +1,128 @@
1
+ from __future__ import annotations
2
+
3
+ from enum import Enum
4
+ from typing import Any
5
+
6
+ from pydantic import BaseModel, Field
7
+
8
+
9
+ class IntentKind(str, Enum):
10
+ RESEARCH = "research"
11
+ WRITING = "writing"
12
+ TRANSFORMATION = "transformation"
13
+ PUBLISHING = "publishing"
14
+ AUTOMATION = "automation"
15
+ ORCHESTRATION = "orchestration"
16
+ DEBUGGING = "debugging"
17
+ ANALYSIS = "analysis"
18
+ CONTENT_GENERATION = "content_generation"
19
+ STRUCTURED_OUTPUT = "structured_output"
20
+ CONVERSATION = "conversation"
21
+
22
+
23
+ class ExecutionClass(str, Enum):
24
+ SIMPLE_REASONING = "simple_reasoning"
25
+ SINGLE_SKILL = "single_skill"
26
+ MULTI_SKILL_PIPELINE = "multi_skill_pipeline"
27
+ RISKY_ACTION = "risky_action"
28
+ REQUIRES_CONFIRMATION = "requires_confirmation"
29
+ REQUIRES_VALIDATION = "requires_validation"
30
+ REQUIRES_FALLBACK = "requires_fallback"
31
+
32
+
33
+ class RiskLevel(str, Enum):
34
+ LOW = "low"
35
+ MEDIUM = "medium"
36
+ HIGH = "high"
37
+
38
+
39
+ class CompletionState(str, Enum):
40
+ FINISHED = "finished"
41
+ PARTIALLY_FINISHED = "partially_finished"
42
+ FAILED = "failed"
43
+ NEEDS_RETRY = "needs_retry"
44
+ NEEDS_HUMAN_CONFIRMATION = "needs_human_confirmation"
45
+
46
+
47
+ class TaskIntent(BaseModel):
48
+ raw_request: str
49
+ objective: str
50
+ primary_intent: IntentKind
51
+ intents: list[IntentKind] = Field(default_factory=list)
52
+ task_type: str = "general"
53
+ hidden_intent: str = ""
54
+ requested_output: str = "clean final response"
55
+ execution_classes: list[ExecutionClass] = Field(default_factory=list)
56
+ risk_level: RiskLevel = RiskLevel.LOW
57
+ notes: list[str] = Field(default_factory=list)
58
+
59
+
60
+ class PlanStep(BaseModel):
61
+ id: str
62
+ action: str
63
+ skill: str | None = None
64
+ tool: str | None = None
65
+ input_spec: dict[str, Any] = Field(default_factory=dict)
66
+ expected_output: str
67
+ validation: str
68
+ risk_note: str = ""
69
+ fallback_skill: str | None = None
70
+ depends_on: list[str] = Field(default_factory=list)
71
+ retry_limit: int = 2
72
+ stop_on_failure: bool = True
73
+ rollback_on_failure: bool = False
74
+
75
+
76
+ class ExecutionPlan(BaseModel):
77
+ objective: str
78
+ task_type: str
79
+ risk_level: RiskLevel
80
+ steps: list[PlanStep] = Field(default_factory=list)
81
+ fallbacks: list[str] = Field(default_factory=list)
82
+ completion_criteria: list[str] = Field(default_factory=list)
83
+
84
+
85
+ class StepExecutionResult(BaseModel):
86
+ step_id: str
87
+ skill: str | None = None
88
+ tool: str | None = None
89
+ status: CompletionState
90
+ output: Any = None
91
+ evidence: list[str] = Field(default_factory=list)
92
+ validation_status: CompletionState = CompletionState.FAILED
93
+ validation_notes: list[str] = Field(default_factory=list)
94
+ attempts: int = 1
95
+ input_snapshot: dict[str, Any] = Field(default_factory=dict)
96
+ trace: list[str] = Field(default_factory=list)
97
+ rolled_back: bool = False
98
+ rollback_notes: list[str] = Field(default_factory=list)
99
+ agent_reviews: list[str] = Field(default_factory=list)
100
+ error: str = ""
101
+
102
+
103
+ class AgentReview(BaseModel):
104
+ agent: str
105
+ status: CompletionState
106
+ notes: list[str] = Field(default_factory=list)
107
+ confidence: float | None = None
108
+
109
+
110
+ class OperatorResult(BaseModel):
111
+ objective: str
112
+ approach_taken: list[str] = Field(default_factory=list)
113
+ result: str
114
+ user_response: str = ""
115
+ technical_details: dict[str, Any] = Field(default_factory=dict)
116
+ validation_status: CompletionState
117
+ risks_or_limitations: list[str] = Field(default_factory=list)
118
+ best_next_action: str
119
+ intent: TaskIntent
120
+ plan: ExecutionPlan
121
+ step_results: list[StepExecutionResult] = Field(default_factory=list)
122
+ artifacts: dict[str, Any] = Field(default_factory=dict)
123
+ mission_trace: list[str] = Field(default_factory=list)
124
+ mission_id: str = ""
125
+ audit_log_path: str = ""
126
+ resumed_from_step: str | None = None
127
+ agent_reviews: list[AgentReview] = Field(default_factory=list)
128
+ provider_telemetry: dict[str, Any] = Field(default_factory=dict)
forge/brain/council.py ADDED
@@ -0,0 +1,244 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+ from typing import Any
5
+
6
+ from forge.brain.contracts import AgentReview, CompletionState, ExecutionPlan, StepExecutionResult
7
+
8
+
9
+ RESEARCH_TERMS = {
10
+ "research",
11
+ "analyze",
12
+ "analysis",
13
+ "summarize",
14
+ "summary",
15
+ "verify",
16
+ "verification",
17
+ "compare",
18
+ "find",
19
+ "inspect",
20
+ "report",
21
+ "brief",
22
+ "researching",
23
+ "تحليل",
24
+ "ابحث",
25
+ "بحث",
26
+ "قارن",
27
+ "تحقق",
28
+ "لخص",
29
+ "تقرير",
30
+ }
31
+ STOPWORDS = {
32
+ "the",
33
+ "and",
34
+ "then",
35
+ "with",
36
+ "from",
37
+ "into",
38
+ "that",
39
+ "this",
40
+ "your",
41
+ "page",
42
+ "file",
43
+ "save",
44
+ "open",
45
+ "click",
46
+ "fill",
47
+ "extract",
48
+ "run",
49
+ }
50
+
51
+
52
+ class ResearchAgent:
53
+ """Prepare browser-heavy missions and convert raw snapshots into verified findings."""
54
+
55
+ def prepare_step(self, request: str, step, payload: dict[str, Any]) -> list[str]:
56
+ notes: list[str] = []
57
+ if step.skill == "browser-executor" and self._is_research_request(request):
58
+ notes.append("Research agent marked this browser step as evidence-first.")
59
+ if step.skill == "file-editor" and "content" not in payload:
60
+ notes.append("Research agent expects file content to be grounded in prior evidence.")
61
+ return notes
62
+
63
+ def enrich_output(self, request: str, step, output: Any) -> tuple[Any, AgentReview | None]:
64
+ if step.skill != "browser-executor" or not isinstance(output, dict):
65
+ return output, None
66
+ if not self._is_research_request(request):
67
+ return output, None
68
+
69
+ enriched = dict(output)
70
+ summary_markdown, confidence, verification = self._summarize_browser_output(request, enriched)
71
+ enriched["research_summary_markdown"] = summary_markdown
72
+ enriched["confidence"] = confidence
73
+ enriched["verification"] = verification
74
+
75
+ evidence = list(enriched.get("evidence", []))
76
+ evidence.append(f"confidence:{confidence:.2f}")
77
+ if verification.get("request_overlap_terms"):
78
+ evidence.append(
79
+ "overlap:" + ", ".join(verification["request_overlap_terms"][:6])
80
+ )
81
+ enriched["evidence"] = list(dict.fromkeys(evidence))
82
+
83
+ status = CompletionState.FINISHED if verification.get("verified") else CompletionState.PARTIALLY_FINISHED
84
+ review = AgentReview(
85
+ agent="research",
86
+ status=status,
87
+ notes=[
88
+ "Research chain completed: navigate -> snapshot -> summarize -> verify.",
89
+ f"Confidence score: {confidence:.2f}.",
90
+ ],
91
+ confidence=confidence,
92
+ )
93
+ return enriched, review
94
+
95
+ @staticmethod
96
+ def _is_research_request(request: str) -> bool:
97
+ lowered = request.lower()
98
+ return any(term in lowered for term in RESEARCH_TERMS)
99
+
100
+ def _summarize_browser_output(self, request: str, output: dict[str, Any]) -> tuple[str, float, dict[str, Any]]:
101
+ page_state = output.get("page_state") or {}
102
+ headings = [self._entry_text(item) for item in page_state.get("headings", []) if isinstance(item, dict)]
103
+ links = [self._entry_text(item) for item in page_state.get("links", []) if isinstance(item, dict)]
104
+ texts = [self._entry_text(item) for item in page_state.get("text", []) if isinstance(item, dict)]
105
+
106
+ request_terms = self._request_terms(request)
107
+ page_terms = self._request_terms(" ".join(headings + texts + links))
108
+ overlap = sorted(request_terms.intersection(page_terms))
109
+ semantic_count = sum(len(value) for value in page_state.values() if isinstance(value, list))
110
+
111
+ confidence = 0.35
112
+ if output.get("current_url"):
113
+ confidence += 0.15
114
+ if semantic_count >= 4:
115
+ confidence += 0.2
116
+ elif semantic_count >= 1:
117
+ confidence += 0.1
118
+ if overlap:
119
+ confidence += min(0.2, 0.04 * len(overlap))
120
+ if str(output.get("snapshot_text", "")).strip():
121
+ confidence += 0.1
122
+ confidence = max(0.15, min(0.95, confidence))
123
+
124
+ verified = bool(output.get("current_url")) and semantic_count > 0
125
+ lines = [
126
+ "# Browser Research Summary",
127
+ f"- Source URL: {output.get('current_url', '') or 'unknown'}",
128
+ f"- Confidence: {confidence:.2f}",
129
+ f"- Verified: {'yes' if verified else 'no'}",
130
+ ]
131
+ if headings:
132
+ lines.append(f"- Headings: {', '.join(headings[:4])}")
133
+ if texts:
134
+ lines.append(f"- Key text: {', '.join(texts[:4])}")
135
+ if links:
136
+ lines.append(f"- Links: {', '.join(links[:4])}")
137
+ if overlap:
138
+ lines.append(f"- Request overlap: {', '.join(overlap[:8])}")
139
+
140
+ verification = {
141
+ "verified": verified,
142
+ "source_url": output.get("current_url", ""),
143
+ "semantic_nodes": semantic_count,
144
+ "request_overlap_terms": overlap[:10],
145
+ }
146
+ return "\n".join(lines), confidence, verification
147
+
148
+ @staticmethod
149
+ def _request_terms(text: str) -> set[str]:
150
+ return {
151
+ token
152
+ for token in re.findall(r"[a-zA-Z0-9_-]{3,}", text.lower())
153
+ if token not in STOPWORDS
154
+ }
155
+
156
+ @staticmethod
157
+ def _entry_text(entry: dict[str, Any]) -> str:
158
+ for key in ("name", "value", "description", "label"):
159
+ value = str(entry.get(key, "")).strip()
160
+ if value:
161
+ return value
162
+ return ""
163
+
164
+
165
+ class ActionAgent:
166
+ """Owns execution dispatch and keeps the mission trace explicit."""
167
+
168
+ @staticmethod
169
+ def dispatch_notes(step) -> list[str]:
170
+ tool_name = step.tool or step.skill or "reasoning"
171
+ return [f"Action agent executing `{tool_name}`."]
172
+
173
+
174
+ class CriticAgent:
175
+ """Reviews each step and the final mission before the user sees it."""
176
+
177
+ def review_step(self, request: str, step, output: Any, validation_status: CompletionState) -> AgentReview:
178
+ notes: list[str] = []
179
+ confidence: float | None = None
180
+ status = validation_status
181
+
182
+ if isinstance(output, dict):
183
+ confidence_value = output.get("confidence")
184
+ if isinstance(confidence_value, (int, float)):
185
+ confidence = float(confidence_value)
186
+ if confidence < 0.55 and status == CompletionState.FINISHED:
187
+ status = CompletionState.PARTIALLY_FINISHED
188
+ notes.append("Critic downgraded this result because confidence is too low.")
189
+
190
+ verification = output.get("verification")
191
+ if isinstance(verification, dict) and not verification.get("verified", True):
192
+ if status == CompletionState.FINISHED:
193
+ status = CompletionState.PARTIALLY_FINISHED
194
+ notes.append("Critic could not fully verify the browser-derived output.")
195
+
196
+ if step.skill == "file-editor" and output.get("changed") is False:
197
+ if status == CompletionState.FINISHED:
198
+ status = CompletionState.PARTIALLY_FINISHED
199
+ notes.append("Critic detected that the file edit produced no change.")
200
+
201
+ if step.skill == "shell-executor" and output.get("exit_code") == 0 and not output.get("stdout") and not output.get("stderr"):
202
+ notes.append("Critic accepted the shell step but flagged minimal observable output.")
203
+
204
+ if not notes:
205
+ notes.append("Critic accepted the step output.")
206
+ return AgentReview(agent="critic", status=status, notes=notes, confidence=confidence)
207
+
208
+ def review_mission(self, plan: ExecutionPlan, step_results: list[StepExecutionResult]) -> AgentReview:
209
+ if not step_results:
210
+ return AgentReview(
211
+ agent="critic",
212
+ status=CompletionState.FAILED,
213
+ notes=["Mission produced no step results."],
214
+ confidence=0.0,
215
+ )
216
+
217
+ statuses = {step.status for step in step_results}
218
+ if CompletionState.FAILED in statuses:
219
+ return AgentReview(
220
+ agent="critic",
221
+ status=CompletionState.PARTIALLY_FINISHED if CompletionState.FINISHED in statuses else CompletionState.FAILED,
222
+ notes=["Mission ended with at least one failed step."],
223
+ confidence=0.45,
224
+ )
225
+ if CompletionState.PARTIALLY_FINISHED in statuses:
226
+ return AgentReview(
227
+ agent="critic",
228
+ status=CompletionState.PARTIALLY_FINISHED,
229
+ notes=["Mission completed, but one or more steps remain partial."],
230
+ confidence=0.65,
231
+ )
232
+ if len(step_results) < len(plan.steps):
233
+ return AgentReview(
234
+ agent="critic",
235
+ status=CompletionState.PARTIALLY_FINISHED,
236
+ notes=["Mission stopped before every planned step executed."],
237
+ confidence=0.55,
238
+ )
239
+ return AgentReview(
240
+ agent="critic",
241
+ status=CompletionState.FINISHED,
242
+ notes=["Mission passed the final critic review."],
243
+ confidence=0.85,
244
+ )