hyperagents 1.2.3rc5.post3__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.
- agents/__init__.py +31 -0
- agents/background_monitor.py +453 -0
- agents/learning.py +460 -0
- agents/planner.py +421 -0
- agents/transcriber.py +341 -0
- civilization/__init__.py +51 -0
- civilization/api/__init__.py +7 -0
- civilization/api/civilization_api.py +361 -0
- civilization/graph/__init__.py +19 -0
- civilization/graph/knowledge_linker.py +348 -0
- civilization/graph/org_graph.py +348 -0
- civilization/graph/process_graph.py +386 -0
- civilization/ingestion/__init__.py +20 -0
- civilization/ingestion/chunker.py +273 -0
- civilization/ingestion/document_ingestor.py +358 -0
- civilization/ingestion/embedder.py +262 -0
- civilization/ingestion/extractor.py +224 -0
- civilization/interview/__init__.py +19 -0
- civilization/interview/gap_detector.py +380 -0
- civilization/interview/interviewer.py +323 -0
- civilization/interview/questioner.py +363 -0
- civilization/retrieval/__init__.py +17 -0
- civilization/retrieval/civilization_rag.py +316 -0
- civilization/retrieval/context_injector.py +342 -0
- civilization/retrieval/relevance_ranker.py +287 -0
- civilization/schema.py +299 -0
- civilization/store.py +177 -0
- civilization/sync/__init__.py +16 -0
- civilization/sync/confluence_sync.py +317 -0
- civilization/sync/gdrive_sync.py +262 -0
- civilization/sync/notion_sync.py +237 -0
- civilization/versioning/__init__.py +19 -0
- civilization/versioning/diff_engine.py +351 -0
- civilization/versioning/staleness_detector.py +303 -0
- civilization/versioning/version_manager.py +293 -0
- cli/__init__.py +1 -0
- cli/doctor.py +334 -0
- cli/hyperclaw.py +1357 -0
- cli/onboarding.py +469 -0
- core/__init__.py +1 -0
- core/connectors/__init__.py +119 -0
- core/connectors/infrastructure.py +772 -0
- core/connectors/messaging.py +586 -0
- core/connectors/productivity.py +764 -0
- core/connectors/smarthome.py +528 -0
- core/connectors/social.py +730 -0
- core/hyperrouter/__init__.py +23 -0
- core/hyperrouter/bandit.py +218 -0
- core/hyperrouter/fast_loop.py +68 -0
- core/hyperrouter/slow_loop.py +143 -0
- core/hyperstate/__init__.py +24 -0
- core/hyperstate/certifier.py +154 -0
- core/hyperstate/schema.py +160 -0
- core/hyperstate/state_manager.py +103 -0
- core/hyperstate/store.py +163 -0
- core/providers/__init__.py +121 -0
- core/providers/embeddings.py +389 -0
- core/providers/image.py +534 -0
- core/providers/llm.py +1935 -0
- core/providers/search.py +608 -0
- core/providers/voice.py +474 -0
- hyperagents-1.2.3rc5.post3.dist-info/METADATA +469 -0
- hyperagents-1.2.3rc5.post3.dist-info/RECORD +361 -0
- hyperagents-1.2.3rc5.post3.dist-info/WHEEL +5 -0
- hyperagents-1.2.3rc5.post3.dist-info/entry_points.txt +6 -0
- hyperagents-1.2.3rc5.post3.dist-info/licenses/LICENSE +21 -0
- hyperagents-1.2.3rc5.post3.dist-info/top_level.txt +13 -0
- hyperclaw/__init__.py +67 -0
- hyperclaw/__main__.py +220 -0
- hyperclaw/agent.py +775 -0
- hyperclaw/agent_coordinator.py +577 -0
- hyperclaw/anthropic_params.py +225 -0
- hyperclaw/api_utils.py +85 -0
- hyperclaw/approval_telegram.py +209 -0
- hyperclaw/approval_terminal.py +80 -0
- hyperclaw/attachments.py +177 -0
- hyperclaw/branding.py +55 -0
- hyperclaw/brief/__init__.py +11 -0
- hyperclaw/brief/cli.py +204 -0
- hyperclaw/brief/collect.py +146 -0
- hyperclaw/brief/compose.py +333 -0
- hyperclaw/brief/diff.py +110 -0
- hyperclaw/brief/errors.py +99 -0
- hyperclaw/brief/google.py +269 -0
- hyperclaw/brief/google_auth.py +294 -0
- hyperclaw/brief/links.py +124 -0
- hyperclaw/brief/service.py +424 -0
- hyperclaw/brief/store.py +205 -0
- hyperclaw/brief/tasks.py +200 -0
- hyperclaw/chat_scope.py +115 -0
- hyperclaw/cli_tui.py +497 -0
- hyperclaw/client_guard.py +717 -0
- hyperclaw/connection_check.py +113 -0
- hyperclaw/cost_guard.py +1728 -0
- hyperclaw/default_config/agents.yaml +534 -0
- hyperclaw/default_config/hyperclaw.yaml +27 -0
- hyperclaw/default_config/models.yaml +161 -0
- hyperclaw/deployment.py +39 -0
- hyperclaw/digest/__init__.py +9 -0
- hyperclaw/digest/cli.py +117 -0
- hyperclaw/digest/errors.py +100 -0
- hyperclaw/digest/gitlab.py +187 -0
- hyperclaw/digest/http.py +113 -0
- hyperclaw/digest/redact.py +32 -0
- hyperclaw/digest/service.py +271 -0
- hyperclaw/digest/slack.py +223 -0
- hyperclaw/digest/state.py +124 -0
- hyperclaw/digest/summarize.py +198 -0
- hyperclaw/digest/tokens.py +143 -0
- hyperclaw/distribution.py +46 -0
- hyperclaw/documents.py +351 -0
- hyperclaw/feeds.py +339 -0
- hyperclaw/google_workspace/__init__.py +5 -0
- hyperclaw/google_workspace/api.py +151 -0
- hyperclaw/google_workspace/auth.py +397 -0
- hyperclaw/google_workspace/calendar.py +157 -0
- hyperclaw/google_workspace/cli.py +72 -0
- hyperclaw/google_workspace/drive.py +236 -0
- hyperclaw/google_workspace/errors.py +58 -0
- hyperclaw/google_workspace/gmail.py +295 -0
- hyperclaw/google_workspace/meet.py +122 -0
- hyperclaw/google_workspace/tools.py +319 -0
- hyperclaw/hyperspeed.py +57 -0
- hyperclaw/integrations_layer.py +1427 -0
- hyperclaw/live_state.py +188 -0
- hyperclaw/managed_config.py +245 -0
- hyperclaw/media_hub.py +179 -0
- hyperclaw/memory_manager.py +584 -0
- hyperclaw/model_catalog.py +551 -0
- hyperclaw/model_policy.py +211 -0
- hyperclaw/model_router.py +595 -0
- hyperclaw/model_selector.py +72 -0
- hyperclaw/naming.py +284 -0
- hyperclaw/notices.py +65 -0
- hyperclaw/oai_stream.py +106 -0
- hyperclaw/orchestrator.py +591 -0
- hyperclaw/outbox.py +82 -0
- hyperclaw/persona.py +367 -0
- hyperclaw/prometheus.py +219 -0
- hyperclaw/prompt_assembly.py +157 -0
- hyperclaw/provider_setup.py +146 -0
- hyperclaw/providers.py +340 -0
- hyperclaw/qualification.py +341 -0
- hyperclaw/qualification_keys/hyper-d292fd127cef5a3e.pub +1 -0
- hyperclaw/qualification_records/anthropic--claude-sonnet-5-5.json +14 -0
- hyperclaw/reasoning_display.py +35 -0
- hyperclaw/scheduler.py +454 -0
- hyperclaw/server.py +847 -0
- hyperclaw/setup.py +627 -0
- hyperclaw/skills.py +717 -0
- hyperclaw/solomon.py +442 -0
- hyperclaw/swarm.py +533 -0
- hyperclaw/taint.py +832 -0
- hyperclaw/team/__init__.py +28 -0
- hyperclaw/team/dispatcher.py +431 -0
- hyperclaw/team/genesis.py +106 -0
- hyperclaw/team/lead.py +110 -0
- hyperclaw/team/model.py +135 -0
- hyperclaw/team/morning.py +238 -0
- hyperclaw/team/repos.py +66 -0
- hyperclaw/team/roster.py +264 -0
- hyperclaw/team/specialist.py +235 -0
- hyperclaw/team/status.py +163 -0
- hyperclaw/team/watchers.py +112 -0
- hyperclaw/teamsetup/__init__.py +17 -0
- hyperclaw/teamsetup/checks.py +208 -0
- hyperclaw/teamsetup/item.py +391 -0
- hyperclaw/teamsetup/oauth_loopback.py +84 -0
- hyperclaw/teamsetup/onboard.py +550 -0
- hyperclaw/teamsetup/profile.py +82 -0
- hyperclaw/teamsetup/secrets.py +313 -0
- hyperclaw/teamsetup/selfcheck.py +295 -0
- hyperclaw/teamsetup/service.py +495 -0
- hyperclaw/teamsetup/slack_oauth.py +442 -0
- hyperclaw/teamsetup/tty.py +62 -0
- hyperclaw/telegram_bot.py +1106 -0
- hyperclaw/telegram_intake.py +126 -0
- hyperclaw/telegram_pairing.py +360 -0
- hyperclaw/terminal_provider.py +323 -0
- hyperclaw/tools/__init__.py +1 -0
- hyperclaw/tools/approval.py +949 -0
- hyperclaw/tools/args.py +212 -0
- hyperclaw/tools/catalog.py +244 -0
- hyperclaw/tools/classify.py +266 -0
- hyperclaw/tools/effects.py +724 -0
- hyperclaw/tools/egress.py +210 -0
- hyperclaw/tools/envelope.py +188 -0
- hyperclaw/tools/guard.py +367 -0
- hyperclaw/tools/ledger.py +339 -0
- hyperclaw/tools/receipts.py +70 -0
- hyperclaw/tools/registry.py +318 -0
- hyperclaw/tools/service_guard.py +151 -0
- hyperclaw/tools/surfaces.py +54 -0
- hyperclaw/tools/workspace.py +87 -0
- hyperclaw/trading/__init__.py +77 -0
- hyperclaw/truth_guard.py +791 -0
- hyperclaw/tts.py +124 -0
- hyperclaw/tui.py +4255 -0
- hyperclaw/tui_bridge.py +938 -0
- hyperclaw/unattended.py +60 -0
- hyperclaw/updates/__init__.py +54 -0
- hyperclaw/updates/check.py +131 -0
- hyperclaw/updates/index.py +178 -0
- hyperclaw/updates/install.py +110 -0
- hyperclaw/updates/managed.py +161 -0
- hyperclaw/updates/manifest.py +138 -0
- hyperclaw/updates/notices.py +86 -0
- hyperclaw/updates/paths.py +53 -0
- hyperclaw/updates/release.py +146 -0
- hyperclaw/updates/upgrade.py +253 -0
- hyperclaw/updates/versioning.py +104 -0
- hyperclaw/userconfig.py +119 -0
- hyperclaw/xai.py +83 -0
- integrations/__init__.py +31 -0
- integrations/automation/__init__.py +1 -0
- integrations/automation/make.py +87 -0
- integrations/automation/zapier.py +87 -0
- integrations/base.py +476 -0
- integrations/communication/__init__.py +1 -0
- integrations/communication/twilio.py +279 -0
- integrations/communication/zoom.py +290 -0
- integrations/data/__init__.py +1 -0
- integrations/data/google_analytics.py +135 -0
- integrations/data/postgres.py +182 -0
- integrations/data/supabase_connector.py +202 -0
- integrations/data/weather_feeds.py +450 -0
- integrations/developer/__init__.py +9 -0
- integrations/developer/github.py +406 -0
- integrations/developer/gitlab.py +298 -0
- integrations/enterprise/__init__.py +13 -0
- integrations/enterprise/confluence.py +266 -0
- integrations/enterprise/hubspot.py +293 -0
- integrations/enterprise/jira.py +314 -0
- integrations/enterprise/salesforce.py +286 -0
- integrations/finance/__init__.py +1 -0
- integrations/finance/polymarket_client.py +247 -0
- integrations/finance/quickbooks.py +281 -0
- integrations/finance/stripe.py +261 -0
- integrations/gateway.py +238 -0
- integrations/messaging/__init__.py +32 -0
- integrations/messaging/discord.py +159 -0
- integrations/messaging/email.py +401 -0
- integrations/messaging/imessage.py +443 -0
- integrations/messaging/signal.py +175 -0
- integrations/messaging/slack.py +217 -0
- integrations/messaging/sms.py +175 -0
- integrations/messaging/teams.py +208 -0
- integrations/messaging/telegram.py +260 -0
- integrations/messaging/whatsapp.py +203 -0
- integrations/productivity/__init__.py +17 -0
- integrations/productivity/airtable.py +217 -0
- integrations/productivity/asana.py +248 -0
- integrations/productivity/google/__init__.py +19 -0
- integrations/productivity/google/base.py +89 -0
- integrations/productivity/google/calendar.py +272 -0
- integrations/productivity/google/docs.py +202 -0
- integrations/productivity/google/drive.py +268 -0
- integrations/productivity/google/gmail.py +279 -0
- integrations/productivity/google/meet.py +218 -0
- integrations/productivity/google/sheets.py +222 -0
- integrations/productivity/google/tasks.py +199 -0
- integrations/productivity/linear.py +279 -0
- integrations/productivity/microsoft/__init__.py +13 -0
- integrations/productivity/microsoft/base.py +93 -0
- integrations/productivity/microsoft/calendar.py +207 -0
- integrations/productivity/microsoft/onedrive.py +222 -0
- integrations/productivity/microsoft/outlook.py +241 -0
- integrations/productivity/microsoft/sharepoint.py +211 -0
- integrations/productivity/notion.py +243 -0
- integrations/productivity/todoist.py +250 -0
- integrations/productivity/trello.py +244 -0
- integrations/storage/__init__.py +1 -0
- integrations/storage/box.py +279 -0
- integrations/storage/dropbox.py +215 -0
- integrations/telegram_inbound.py +179 -0
- integrations/telegram_poller.py +151 -0
- memory/__init__.py +1 -0
- memory/causal_graph.py +125 -0
- memory/impact_tracker.py +88 -0
- memory/vector_memory.py +436 -0
- models/__init__.py +12 -0
- models/chatjimmy_client.py +210 -0
- models/claude_client.py +235 -0
- models/claude_code_subagent.py +192 -0
- models/openai_client.py +120 -0
- models/router.py +162 -0
- recursive/discovery_loop.py +118 -0
- recursive/optimization_loop.py +135 -0
- recursive/validation_loop.py +38 -0
- security/__init__.py +1 -0
- security/audit_logger.py +106 -0
- security/filesystem_guard.py +100 -0
- security/hypershield.py +99 -0
- security/network_guard.py +97 -0
- security/policy_engine.py +198 -0
- swarm/__init__.py +1 -0
- swarm/agents/__init__.py +0 -0
- swarm/agents/base.py +230 -0
- swarm/agents/business/__init__.py +0 -0
- swarm/agents/business/counsel.py +31 -0
- swarm/agents/business/herald.py +32 -0
- swarm/agents/business/ledger.py +26 -0
- swarm/agents/business/nexus.py +30 -0
- swarm/agents/business/ops.py +30 -0
- swarm/agents/business/pipeline.py +28 -0
- swarm/agents/business/revenue.py +30 -0
- swarm/agents/business/sovereign.py +31 -0
- swarm/agents/business/strategos.py +27 -0
- swarm/agents/business/talent.py +27 -0
- swarm/agents/business/venture.py +32 -0
- swarm/agents/comms/__init__.py +1 -0
- swarm/agents/comms/cipher.py +32 -0
- swarm/agents/comms/echo_comms.py +31 -0
- swarm/agents/comms/envoy.py +31 -0
- swarm/agents/comms/herald_scheduler.py +33 -0
- swarm/agents/comms/pulse.py +29 -0
- swarm/agents/creative/__init__.py +0 -0
- swarm/agents/creative/author.py +27 -0
- swarm/agents/creative/lens.py +25 -0
- swarm/agents/executive/__init__.py +1 -0
- swarm/agents/intelligence/__init__.py +1 -0
- swarm/agents/intelligence/arbiter.py +30 -0
- swarm/agents/intelligence/sentinel.py +30 -0
- swarm/agents/media/__init__.py +0 -0
- swarm/agents/personal/__init__.py +0 -0
- swarm/agents/personal/atlas.py +27 -0
- swarm/agents/personal/hearth.py +26 -0
- swarm/agents/personal/midas.py +27 -0
- swarm/agents/personal/navigator.py +27 -0
- swarm/agents/personal/nourish.py +31 -0
- swarm/agents/personal/vitals.py +33 -0
- swarm/agents/recursive/__init__.py +0 -0
- swarm/agents/recursive/alchemist.py +73 -0
- swarm/agents/recursive/calibrator.py +45 -0
- swarm/agents/recursive/scout.py +97 -0
- swarm/agents/scientific/__init__.py +0 -0
- swarm/agents/scientific/cosmos.py +36 -0
- swarm/agents/scientific/gaia.py +26 -0
- swarm/agents/scientific/medicus.py +33 -0
- swarm/agents/scientific/oracle_agent.py +39 -0
- swarm/agents/scientific/scribe.py +39 -0
- swarm/agents/talent/__init__.py +0 -0
- swarm/agents/talent/deal.py +28 -0
- swarm/agents/talent/roster.py +29 -0
- swarm/agents/talent/scout.py +29 -0
- swarm/agents/talent/stage.py +27 -0
- swarm/agents/tech/__init__.py +0 -0
- swarm/agents/tech/aegis.py +28 -0
- swarm/agents/tech/bridge.py +28 -0
- swarm/agents/tech/forge.py +28 -0
- swarm/agents/trading/__init__.py +1 -0
- swarm/agents/trading/global_prediction_engine.py +611 -0
- swarm/agents/trading/polymarket_trader.py +165 -0
- swarm/agents/trading/prediction_strategist.py +218 -0
- swarm/autogen_bridge.py +68 -0
- swarm/bid_protocol.py +196 -0
- swarm/dispatcher.py +527 -0
- swarm/nexus.py +181 -0
- swarm/registry.py +112 -0
- telegram_listener.py +403 -0
- ui/tui/app.py +214 -0
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
"""
|
|
2
|
+
The effect ledger (tool standard S3) and activity log (spec rule 6).
|
|
3
|
+
|
|
4
|
+
A local, append-only SQLite table at $HYPERCLAW_ROOT/ledger/effects.sqlite3
|
|
5
|
+
(default ~/.hyperclaw). Every non-read tool call leaves rows here:
|
|
6
|
+
|
|
7
|
+
- an intent row (status "pending") written BEFORE the tool runs, in the same
|
|
8
|
+
transaction as the idempotency check (BEGIN IMMEDIATE, so two processes —
|
|
9
|
+
the terminal and the Telegram bridge — cannot both pass the check);
|
|
10
|
+
- an outcome row (ok / failed / uncertain / ...) after it ran, pointing at
|
|
11
|
+
the intent (`ref`);
|
|
12
|
+
- a single row for a call the runtime refused (duplicate, needs_approval,
|
|
13
|
+
budget_exhausted).
|
|
14
|
+
|
|
15
|
+
UPDATE and DELETE are blocked by triggers. A pending row with no outcome
|
|
16
|
+
(the process died mid-call) is treated as uncertain: at-most-once.
|
|
17
|
+
|
|
18
|
+
Decisions (per conversation, within the window):
|
|
19
|
+
- same key, latest row ok -> duplicate (original receipt), not run
|
|
20
|
+
(file targets: only while the file still has the content that was written)
|
|
21
|
+
- same key, latest row uncertain/pending -> needs_approval ("may already have
|
|
22
|
+
been sent"), not run (file targets: checked on disk instead)
|
|
23
|
+
- same target, different payload, send/spend/delete, inside the shorter
|
|
24
|
+
conflict window -> needs_approval, not run
|
|
25
|
+
- otherwise -> intent row, run
|
|
26
|
+
|
|
27
|
+
Broker mapping: `key` is the agent-mode proposal `payloadDigest`; this table
|
|
28
|
+
is the local equivalent of `agent_action_ledger` (append-only, one row per
|
|
29
|
+
state change, receipt on the result).
|
|
30
|
+
"""
|
|
31
|
+
from __future__ import annotations
|
|
32
|
+
|
|
33
|
+
import json
|
|
34
|
+
import os
|
|
35
|
+
import sqlite3
|
|
36
|
+
import threading
|
|
37
|
+
import time
|
|
38
|
+
from dataclasses import dataclass
|
|
39
|
+
from pathlib import Path
|
|
40
|
+
from typing import Any, Dict, List, Optional
|
|
41
|
+
|
|
42
|
+
DEFAULT_WINDOW_S = 24 * 3600
|
|
43
|
+
DEFAULT_CONFLICT_WINDOW_S = 600
|
|
44
|
+
|
|
45
|
+
# Statuses that mean "an effect happened or may have happened".
|
|
46
|
+
DONE = ("ok",)
|
|
47
|
+
MAYBE = ("uncertain", "pending")
|
|
48
|
+
REFUSALS = ("duplicate", "needs_approval", "budget_exhausted")
|
|
49
|
+
CONFLICT_EFFECTS = ("send", "spend", "delete")
|
|
50
|
+
|
|
51
|
+
_SCHEMA = """
|
|
52
|
+
CREATE TABLE IF NOT EXISTS effect_ledger (
|
|
53
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
54
|
+
ts REAL NOT NULL,
|
|
55
|
+
conversation TEXT NOT NULL,
|
|
56
|
+
turn TEXT,
|
|
57
|
+
surface TEXT,
|
|
58
|
+
tool TEXT NOT NULL,
|
|
59
|
+
effect TEXT NOT NULL,
|
|
60
|
+
target TEXT,
|
|
61
|
+
payload_hash TEXT,
|
|
62
|
+
key TEXT,
|
|
63
|
+
status TEXT NOT NULL,
|
|
64
|
+
receipt_id TEXT,
|
|
65
|
+
ref INTEGER,
|
|
66
|
+
post_hash TEXT,
|
|
67
|
+
args TEXT,
|
|
68
|
+
message TEXT
|
|
69
|
+
);
|
|
70
|
+
CREATE INDEX IF NOT EXISTS effect_ledger_key ON effect_ledger (conversation, key, id);
|
|
71
|
+
CREATE INDEX IF NOT EXISTS effect_ledger_target ON effect_ledger (conversation, target, id);
|
|
72
|
+
CREATE INDEX IF NOT EXISTS effect_ledger_ref ON effect_ledger (ref);
|
|
73
|
+
CREATE TRIGGER IF NOT EXISTS effect_ledger_no_update BEFORE UPDATE ON effect_ledger
|
|
74
|
+
BEGIN SELECT RAISE(ABORT, 'effect_ledger is append-only'); END;
|
|
75
|
+
CREATE TRIGGER IF NOT EXISTS effect_ledger_no_delete BEFORE DELETE ON effect_ledger
|
|
76
|
+
BEGIN SELECT RAISE(ABORT, 'effect_ledger is append-only'); END;
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
_COLUMNS = ("id", "ts", "conversation", "turn", "surface", "tool", "effect", "target",
|
|
80
|
+
"payload_hash", "key", "status", "receipt_id", "ref", "post_hash", "args", "message")
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _env_float(name: str, default: float) -> float:
|
|
84
|
+
try:
|
|
85
|
+
return float(os.environ.get(name, default))
|
|
86
|
+
except (TypeError, ValueError):
|
|
87
|
+
return float(default)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def default_path() -> Path:
|
|
91
|
+
"""$HYPERCLAW_EFFECT_LEDGER if set, else $HYPERCLAW_ROOT/ledger/effects.sqlite3
|
|
92
|
+
(HYPERCLAW_ROOT defaults to ~/.hyperclaw)."""
|
|
93
|
+
explicit = os.environ.get("HYPERCLAW_EFFECT_LEDGER")
|
|
94
|
+
if explicit:
|
|
95
|
+
return Path(explicit).expanduser()
|
|
96
|
+
root = Path(os.environ.get("HYPERCLAW_ROOT", Path.home() / ".hyperclaw")).expanduser()
|
|
97
|
+
return root / "ledger" / "effects.sqlite3"
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@dataclass
|
|
101
|
+
class Decision:
|
|
102
|
+
action: str # "proceed" | "duplicate" | "needs_approval"
|
|
103
|
+
intent_id: Optional[int] = None
|
|
104
|
+
prior: Optional[Dict[str, Any]] = None
|
|
105
|
+
reason: str = "" # "duplicate" | "uncertain" | "conflict"
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class EffectLedger:
|
|
109
|
+
def __init__(self, path: Optional[os.PathLike] = None):
|
|
110
|
+
self.path = Path(path) if path else default_path()
|
|
111
|
+
self._init_lock = threading.Lock()
|
|
112
|
+
self._ready = False
|
|
113
|
+
|
|
114
|
+
# ── connection ──
|
|
115
|
+
def _connect(self) -> sqlite3.Connection:
|
|
116
|
+
if not self._ready:
|
|
117
|
+
with self._init_lock:
|
|
118
|
+
if not self._ready:
|
|
119
|
+
self.path.parent.mkdir(parents=True, exist_ok=True)
|
|
120
|
+
try:
|
|
121
|
+
os.chmod(self.path.parent, 0o700)
|
|
122
|
+
except OSError:
|
|
123
|
+
pass
|
|
124
|
+
con = sqlite3.connect(self.path, timeout=10, isolation_level=None)
|
|
125
|
+
try:
|
|
126
|
+
con.execute("PRAGMA journal_mode=WAL")
|
|
127
|
+
con.executescript(_SCHEMA)
|
|
128
|
+
finally:
|
|
129
|
+
con.close()
|
|
130
|
+
try:
|
|
131
|
+
os.chmod(self.path, 0o600)
|
|
132
|
+
except OSError:
|
|
133
|
+
pass
|
|
134
|
+
self._ready = True
|
|
135
|
+
con = sqlite3.connect(self.path, timeout=10, isolation_level=None)
|
|
136
|
+
con.row_factory = sqlite3.Row
|
|
137
|
+
return con
|
|
138
|
+
|
|
139
|
+
@staticmethod
|
|
140
|
+
def window_s() -> float:
|
|
141
|
+
return _env_float("HYPERCLAW_EFFECT_WINDOW_S", DEFAULT_WINDOW_S)
|
|
142
|
+
|
|
143
|
+
@staticmethod
|
|
144
|
+
def conflict_window_s() -> float:
|
|
145
|
+
return _env_float("HYPERCLAW_EFFECT_CONFLICT_WINDOW_S", DEFAULT_CONFLICT_WINDOW_S)
|
|
146
|
+
|
|
147
|
+
@staticmethod
|
|
148
|
+
def _insert(con, row: Dict[str, Any]) -> int:
|
|
149
|
+
row = dict(row)
|
|
150
|
+
row.setdefault("ts", time.time())
|
|
151
|
+
cols = [c for c in _COLUMNS if c != "id" and c in row]
|
|
152
|
+
cur = con.execute(
|
|
153
|
+
f"INSERT INTO effect_ledger ({','.join(cols)}) VALUES ({','.join('?' * len(cols))})",
|
|
154
|
+
[row[c] for c in cols])
|
|
155
|
+
return int(cur.lastrowid)
|
|
156
|
+
|
|
157
|
+
# ── the check + intent, in one transaction ──
|
|
158
|
+
def begin(self, *, conversation: str, turn: str, surface: str, tool: str, effect: str,
|
|
159
|
+
target: str, payload_hash: str, key: Optional[str], args: str = "",
|
|
160
|
+
current_file_hash: Optional[str] = None,
|
|
161
|
+
expected_post_hash: Optional[str] = None,
|
|
162
|
+
is_file: bool = False, allow_conflict: bool = False) -> Decision:
|
|
163
|
+
"""allow_conflict: the owner approved this exact payload (approval
|
|
164
|
+
layer), so a different earlier payload to the same target does not
|
|
165
|
+
hold it. Exact duplicates and uncertain repeats are still held."""
|
|
166
|
+
now = time.time()
|
|
167
|
+
base = dict(ts=now, conversation=conversation, turn=turn, surface=surface, tool=tool,
|
|
168
|
+
effect=effect, target=target, payload_hash=payload_hash, key=key, args=args)
|
|
169
|
+
con = self._connect()
|
|
170
|
+
try:
|
|
171
|
+
con.execute("BEGIN IMMEDIATE")
|
|
172
|
+
decision = self._decide(con, now, conversation, effect, target, key,
|
|
173
|
+
current_file_hash, expected_post_hash, is_file)
|
|
174
|
+
if allow_conflict and decision.reason == "conflict":
|
|
175
|
+
decision = Decision("proceed")
|
|
176
|
+
if decision.action == "proceed":
|
|
177
|
+
decision.intent_id = self._insert(con, {**base, "status": "pending"})
|
|
178
|
+
else:
|
|
179
|
+
prior = decision.prior or {}
|
|
180
|
+
self._insert(con, {**base, "status": decision.action,
|
|
181
|
+
"receipt_id": prior.get("receipt_id"),
|
|
182
|
+
"ref": prior.get("id"),
|
|
183
|
+
"message": decision.reason})
|
|
184
|
+
con.execute("COMMIT")
|
|
185
|
+
return decision
|
|
186
|
+
except Exception:
|
|
187
|
+
try:
|
|
188
|
+
con.execute("ROLLBACK")
|
|
189
|
+
except sqlite3.Error:
|
|
190
|
+
pass
|
|
191
|
+
raise
|
|
192
|
+
finally:
|
|
193
|
+
con.close()
|
|
194
|
+
|
|
195
|
+
def check(self, *, conversation: str, effect: str, target: str, key: Optional[str],
|
|
196
|
+
current_file_hash: Optional[str] = None,
|
|
197
|
+
expected_post_hash: Optional[str] = None, is_file: bool = False) -> Decision:
|
|
198
|
+
"""The same decision as begin(), without writing anything."""
|
|
199
|
+
con = self._connect()
|
|
200
|
+
try:
|
|
201
|
+
return self._decide(con, time.time(), conversation, effect, target, key,
|
|
202
|
+
current_file_hash, expected_post_hash, is_file)
|
|
203
|
+
finally:
|
|
204
|
+
con.close()
|
|
205
|
+
|
|
206
|
+
def _decide(self, con, now, conversation, effect, target, key, current_file_hash,
|
|
207
|
+
expected_post_hash, is_file) -> Decision:
|
|
208
|
+
if key:
|
|
209
|
+
row = con.execute(
|
|
210
|
+
"SELECT * FROM effect_ledger WHERE conversation=? AND key=? AND ts>=? "
|
|
211
|
+
f"AND status NOT IN ({','.join('?' * len(REFUSALS))}) ORDER BY id DESC LIMIT 1",
|
|
212
|
+
(conversation, key, now - self.window_s(), *REFUSALS)).fetchone()
|
|
213
|
+
if row is not None:
|
|
214
|
+
prior = dict(row)
|
|
215
|
+
st = prior["status"]
|
|
216
|
+
if st in DONE:
|
|
217
|
+
if not is_file:
|
|
218
|
+
return Decision("duplicate", prior=prior, reason="duplicate")
|
|
219
|
+
# A file effect is "already done" only while the file still
|
|
220
|
+
# holds what was written; edits since then make it new.
|
|
221
|
+
if prior.get("post_hash") and prior["post_hash"] == current_file_hash:
|
|
222
|
+
return Decision("duplicate", prior=prior, reason="duplicate")
|
|
223
|
+
elif st in MAYBE:
|
|
224
|
+
if is_file:
|
|
225
|
+
if expected_post_hash and expected_post_hash == current_file_hash:
|
|
226
|
+
return Decision("duplicate", prior=prior, reason="duplicate")
|
|
227
|
+
else:
|
|
228
|
+
return Decision("needs_approval", prior=prior, reason="uncertain")
|
|
229
|
+
if effect in CONFLICT_EFFECTS and target:
|
|
230
|
+
row = con.execute(
|
|
231
|
+
"SELECT * FROM effect_ledger r WHERE r.conversation=? AND r.target=? "
|
|
232
|
+
"AND r.effect=? AND r.ts>=? AND (r.key IS NULL OR r.key<>?) AND ("
|
|
233
|
+
" r.status IN ('ok','uncertain') OR (r.status='pending' AND NOT EXISTS "
|
|
234
|
+
" (SELECT 1 FROM effect_ledger o WHERE o.ref=r.id AND o.status NOT IN "
|
|
235
|
+
f"({','.join('?' * len(REFUSALS))})))) "
|
|
236
|
+
"ORDER BY r.id DESC LIMIT 1",
|
|
237
|
+
(conversation, target, effect, now - self.conflict_window_s(),
|
|
238
|
+
key or "", *REFUSALS)).fetchone()
|
|
239
|
+
if row is not None:
|
|
240
|
+
return Decision("needs_approval", prior=dict(row), reason="conflict")
|
|
241
|
+
return Decision("proceed")
|
|
242
|
+
|
|
243
|
+
# ── outcomes and plain records ──
|
|
244
|
+
def finish(self, intent_id: int, *, status: str, receipt_id: Optional[str] = None,
|
|
245
|
+
post_hash: Optional[str] = None, message: str = "") -> int:
|
|
246
|
+
con = self._connect()
|
|
247
|
+
try:
|
|
248
|
+
con.execute("BEGIN IMMEDIATE")
|
|
249
|
+
row = con.execute("SELECT * FROM effect_ledger WHERE id=?", (intent_id,)).fetchone()
|
|
250
|
+
if row is None:
|
|
251
|
+
con.execute("ROLLBACK")
|
|
252
|
+
raise KeyError(intent_id)
|
|
253
|
+
base = {k: row[k] for k in ("conversation", "turn", "surface", "tool", "effect",
|
|
254
|
+
"target", "payload_hash", "key", "args")}
|
|
255
|
+
rid = self._insert(con, {**base, "status": status, "receipt_id": receipt_id,
|
|
256
|
+
"ref": intent_id, "post_hash": post_hash,
|
|
257
|
+
"message": (message or "")[:500]})
|
|
258
|
+
con.execute("COMMIT")
|
|
259
|
+
return rid
|
|
260
|
+
finally:
|
|
261
|
+
con.close()
|
|
262
|
+
|
|
263
|
+
def record(self, **row) -> int:
|
|
264
|
+
"""Append one finished row (bash file effects, budget refusals)."""
|
|
265
|
+
row.setdefault("message", "")
|
|
266
|
+
row["message"] = (row["message"] or "")[:500]
|
|
267
|
+
con = self._connect()
|
|
268
|
+
try:
|
|
269
|
+
return self._insert(con, row)
|
|
270
|
+
finally:
|
|
271
|
+
con.close()
|
|
272
|
+
|
|
273
|
+
# ── readers ──
|
|
274
|
+
def rows(self, conversation: Optional[str] = None, limit: int = 1000) -> List[Dict[str, Any]]:
|
|
275
|
+
con = self._connect()
|
|
276
|
+
try:
|
|
277
|
+
if conversation:
|
|
278
|
+
cur = con.execute("SELECT * FROM effect_ledger WHERE conversation=? "
|
|
279
|
+
"ORDER BY id DESC LIMIT ?", (conversation, limit))
|
|
280
|
+
else:
|
|
281
|
+
cur = con.execute("SELECT * FROM effect_ledger ORDER BY id DESC LIMIT ?", (limit,))
|
|
282
|
+
return [dict(r) for r in cur.fetchall()][::-1]
|
|
283
|
+
finally:
|
|
284
|
+
con.close()
|
|
285
|
+
|
|
286
|
+
def activity(self, conversation: Optional[str] = None, limit: int = 20) -> List[Dict[str, Any]]:
|
|
287
|
+
"""The activity log: one entry per action (an intent and its outcome
|
|
288
|
+
collapsed), newest last. An intent with no outcome shows as
|
|
289
|
+
'pending (outcome unknown)'."""
|
|
290
|
+
con = self._connect()
|
|
291
|
+
try:
|
|
292
|
+
where, params = ("WHERE r.conversation=?", [conversation]) if conversation else ("", [])
|
|
293
|
+
cur = con.execute(
|
|
294
|
+
"SELECT r.* FROM effect_ledger r "
|
|
295
|
+
f"{where} {'AND' if where else 'WHERE'} NOT (r.status='pending' AND EXISTS "
|
|
296
|
+
"(SELECT 1 FROM effect_ledger o WHERE o.ref=r.id AND o.status NOT IN "
|
|
297
|
+
f"({','.join('?' * len(REFUSALS))}))) "
|
|
298
|
+
"ORDER BY r.id DESC LIMIT ?", [*params, *REFUSALS, limit])
|
|
299
|
+
return [dict(r) for r in cur.fetchall()][::-1]
|
|
300
|
+
finally:
|
|
301
|
+
con.close()
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def format_activity(rows: List[Dict[str, Any]]) -> str:
|
|
305
|
+
if not rows:
|
|
306
|
+
return "No activity recorded."
|
|
307
|
+
lines = []
|
|
308
|
+
for r in rows:
|
|
309
|
+
when = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(r["ts"]))
|
|
310
|
+
status = r["status"] if r["status"] != "pending" else "pending (outcome unknown)"
|
|
311
|
+
target = r.get("target") or ""
|
|
312
|
+
if target.startswith(r["tool"] + ":"):
|
|
313
|
+
target = target[len(r["tool"]) + 1:]
|
|
314
|
+
line = f"{when} {r['conversation']:<14} {r['tool']:<14} {r['effect']:<7} {status:<15}"
|
|
315
|
+
if target:
|
|
316
|
+
line += f" {target[:70]}"
|
|
317
|
+
if r.get("receipt_id"):
|
|
318
|
+
line += f" [{r['receipt_id']}]"
|
|
319
|
+
if r.get("message") and r["status"] != "ok":
|
|
320
|
+
line += f" — {r['message'][:120]}"
|
|
321
|
+
lines.append(line)
|
|
322
|
+
return "\n".join(lines)
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def activity_json(rows: List[Dict[str, Any]]) -> str:
|
|
326
|
+
return json.dumps(rows, ensure_ascii=False, default=str, indent=1)
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
_LEDGERS: Dict[str, EffectLedger] = {}
|
|
330
|
+
_LEDGERS_LOCK = threading.Lock()
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def get_ledger() -> EffectLedger:
|
|
334
|
+
"""The ledger for the current HYPERCLAW_ROOT (read at call time)."""
|
|
335
|
+
p = str(default_path())
|
|
336
|
+
with _LEDGERS_LOCK:
|
|
337
|
+
if p not in _LEDGERS:
|
|
338
|
+
_LEDGERS[p] = EffectLedger(p)
|
|
339
|
+
return _LEDGERS[p]
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Send receipts (benchmark 20.3, rc3).
|
|
3
|
+
|
|
4
|
+
A send tool reports success only with a provider-confirmed receipt:
|
|
5
|
+
|
|
6
|
+
- Telegram: HTTP 200 AND ``ok: true`` AND ``result.message_id`` -> the
|
|
7
|
+
message id is the receipt. ``ok: false`` (e.g. "Bad Request: chat not
|
|
8
|
+
found") or a missing id is a failure carrying the provider's description.
|
|
9
|
+
- Discord (webhook with ``?wait=true``): the returned message ``id``.
|
|
10
|
+
- Gmail: the sent message ``id``.
|
|
11
|
+
|
|
12
|
+
Success strings carry the provider id as ``[id:<id>]``; the classifier
|
|
13
|
+
(tools/classify.py) requires it for these channels and returns ``failed``
|
|
14
|
+
without it, so "Message sent" can never stand without a receipt.
|
|
15
|
+
Provider descriptions are redacted (no tokens or keys) and clipped.
|
|
16
|
+
"""
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import re
|
|
20
|
+
from typing import Optional, Tuple
|
|
21
|
+
|
|
22
|
+
RECEIPT_RE = r"\[id:([^\]\s]+)\]"
|
|
23
|
+
_BOT_URL = re.compile(r"/bot[^/\s]+/")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def redact(text: str, limit: int = 200) -> str:
|
|
27
|
+
"""Provider text safe to show and log: secrets removed, clipped."""
|
|
28
|
+
from hyperclaw.truth_guard import redact_secrets
|
|
29
|
+
t = _BOT_URL.sub("/bot[REDACTED]/", str(text or ""))
|
|
30
|
+
return redact_secrets(t).strip()[:limit]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _json(resp) -> Optional[dict]:
|
|
34
|
+
try:
|
|
35
|
+
body = resp.json()
|
|
36
|
+
except Exception:
|
|
37
|
+
return None
|
|
38
|
+
return body if isinstance(body, dict) else None
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def telegram_receipt(resp) -> Tuple[Optional[str], str]:
|
|
42
|
+
"""(message_id, "") when Telegram confirmed delivery, else (None, why)."""
|
|
43
|
+
body = _json(resp)
|
|
44
|
+
status = getattr(resp, "status_code", None)
|
|
45
|
+
if body is None:
|
|
46
|
+
return None, redact(f"HTTP {status}: response was not Telegram JSON")
|
|
47
|
+
if body.get("ok") is True and status == 200:
|
|
48
|
+
res = body.get("result")
|
|
49
|
+
mid = res.get("message_id") if isinstance(res, dict) else None
|
|
50
|
+
if mid not in (None, ""):
|
|
51
|
+
return str(mid), ""
|
|
52
|
+
return None, "Telegram returned no message_id, so delivery is not confirmed"
|
|
53
|
+
desc = body.get("description") or f"HTTP {status}"
|
|
54
|
+
return None, redact(desc)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def discord_receipt(resp) -> Tuple[Optional[str], str]:
|
|
58
|
+
body = _json(resp)
|
|
59
|
+
status = getattr(resp, "status_code", None)
|
|
60
|
+
if status == 200 and body and body.get("id"):
|
|
61
|
+
return str(body["id"]), ""
|
|
62
|
+
if status == 204:
|
|
63
|
+
return None, "Discord accepted the message but returned no message id (use ?wait=true)"
|
|
64
|
+
desc = (body or {}).get("message") or getattr(resp, "text", "") or f"HTTP {status}"
|
|
65
|
+
return None, redact(f"HTTP {status}: {desc}")
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def has_receipt(text: str) -> Optional[str]:
|
|
69
|
+
m = re.search(RECEIPT_RE, text or "")
|
|
70
|
+
return m.group(1) if m else None
|