proseforge-agent 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- proseforge_agent/__init__.py +10 -0
- proseforge_agent/__main__.py +8 -0
- proseforge_agent/_bootstrap.py +108 -0
- proseforge_agent/agent/__init__.py +123 -0
- proseforge_agent/agent/artifacts.py +160 -0
- proseforge_agent/agent/attachments.py +282 -0
- proseforge_agent/agent/audit.py +198 -0
- proseforge_agent/agent/context_window.py +90 -0
- proseforge_agent/agent/control.py +45 -0
- proseforge_agent/agent/degradation.py +166 -0
- proseforge_agent/agent/eval.py +237 -0
- proseforge_agent/agent/events.py +209 -0
- proseforge_agent/agent/execution_guard.py +219 -0
- proseforge_agent/agent/function_calling.py +255 -0
- proseforge_agent/agent/intent_router.py +152 -0
- proseforge_agent/agent/kernel.py +514 -0
- proseforge_agent/agent/loop.py +301 -0
- proseforge_agent/agent/middleware.py +241 -0
- proseforge_agent/agent/modes.py +26 -0
- proseforge_agent/agent/observability.py +385 -0
- proseforge_agent/agent/offline.py +77 -0
- proseforge_agent/agent/permissions.py +101 -0
- proseforge_agent/agent/planner.py +166 -0
- proseforge_agent/agent/profiles.py +122 -0
- proseforge_agent/agent/prompt_templates.py +230 -0
- proseforge_agent/agent/provider_fallback.py +135 -0
- proseforge_agent/agent/reflection.py +136 -0
- proseforge_agent/agent/request_cache.py +186 -0
- proseforge_agent/agent/safety.py +146 -0
- proseforge_agent/agent/sandbox.py +205 -0
- proseforge_agent/agent/structured_output.py +236 -0
- proseforge_agent/agent/subagent.py +111 -0
- proseforge_agent/agent/tools.py +391 -0
- proseforge_agent/agent/types.py +60 -0
- proseforge_agent/capabilities.py +194 -0
- proseforge_agent/chapter/__init__.py +67 -0
- proseforge_agent/chapter/accept.py +81 -0
- proseforge_agent/chapter/context.py +113 -0
- proseforge_agent/chapter/draft.py +137 -0
- proseforge_agent/chapter/lifecycle.py +293 -0
- proseforge_agent/chapter/review.py +162 -0
- proseforge_agent/chapter/rewrite.py +187 -0
- proseforge_agent/chat/__init__.py +60 -0
- proseforge_agent/chat/context.py +66 -0
- proseforge_agent/chat/handoff.py +111 -0
- proseforge_agent/chat/memory.py +115 -0
- proseforge_agent/chat/prompts.py +129 -0
- proseforge_agent/chat/repl.py +207 -0
- proseforge_agent/chat/retrieval.py +127 -0
- proseforge_agent/chat/session.py +934 -0
- proseforge_agent/chat/slash.py +134 -0
- proseforge_agent/chat/system_prompts.py +221 -0
- proseforge_agent/chat/transcript.py +39 -0
- proseforge_agent/cli.py +6746 -0
- proseforge_agent/concurrency.py +156 -0
- proseforge_agent/config.py +138 -0
- proseforge_agent/cron/__init__.py +21 -0
- proseforge_agent/cron/core.py +168 -0
- proseforge_agent/daily/__init__.py +17 -0
- proseforge_agent/daily/recommend.py +84 -0
- proseforge_agent/daily/workbook.py +169 -0
- proseforge_agent/demo.py +281 -0
- proseforge_agent/dotenv.py +126 -0
- proseforge_agent/environments/__init__.py +40 -0
- proseforge_agent/environments/base.py +124 -0
- proseforge_agent/environments/checkpoints.py +45 -0
- proseforge_agent/environments/daytona.py +21 -0
- proseforge_agent/environments/docker.py +64 -0
- proseforge_agent/environments/file_sync.py +60 -0
- proseforge_agent/environments/local.py +58 -0
- proseforge_agent/environments/modal.py +21 -0
- proseforge_agent/environments/process_registry.py +151 -0
- proseforge_agent/environments/process_runner.py +99 -0
- proseforge_agent/environments/serverless.py +46 -0
- proseforge_agent/environments/singularity.py +45 -0
- proseforge_agent/environments/ssh.py +47 -0
- proseforge_agent/errors.py +44 -0
- proseforge_agent/eval/__init__.py +17 -0
- proseforge_agent/eval/trajectories.py +140 -0
- proseforge_agent/extensions/__init__.py +24 -0
- proseforge_agent/extensions/base.py +114 -0
- proseforge_agent/extensions/registry.py +100 -0
- proseforge_agent/gateway/__init__.py +30 -0
- proseforge_agent/gateway/core.py +139 -0
- proseforge_agent/gateway/delivery.py +117 -0
- proseforge_agent/gateway/media.py +99 -0
- proseforge_agent/gateway/platforms/__init__.py +32 -0
- proseforge_agent/gateway/platforms/base.py +164 -0
- proseforge_agent/gateway/platforms/discord.py +82 -0
- proseforge_agent/gateway/platforms/email.py +33 -0
- proseforge_agent/gateway/platforms/mobile_email.py +65 -0
- proseforge_agent/gateway/platforms/signal.py +31 -0
- proseforge_agent/gateway/platforms/slack.py +81 -0
- proseforge_agent/gateway/platforms/telegram.py +199 -0
- proseforge_agent/gateway/platforms/telegram_transport.py +119 -0
- proseforge_agent/gateway/platforms/whatsapp.py +31 -0
- proseforge_agent/gateway/poller.py +76 -0
- proseforge_agent/gateway/relay/__init__.py +5 -0
- proseforge_agent/gateway/relay/auth.py +155 -0
- proseforge_agent/install/__init__.py +60 -0
- proseforge_agent/install/app_dirs.py +97 -0
- proseforge_agent/install/auto_trigger.py +118 -0
- proseforge_agent/install/binary_build.py +147 -0
- proseforge_agent/install/binary_packaging.py +63 -0
- proseforge_agent/install/ci_matrix.py +98 -0
- proseforge_agent/install/docker_plan.py +53 -0
- proseforge_agent/install/doctor.py +263 -0
- proseforge_agent/install/first_run.py +87 -0
- proseforge_agent/install/installer_scripts.py +195 -0
- proseforge_agent/install/installers.py +217 -0
- proseforge_agent/install/linux.py +70 -0
- proseforge_agent/install/local_models.py +98 -0
- proseforge_agent/install/macos.py +66 -0
- proseforge_agent/install/migrations.py +98 -0
- proseforge_agent/install/package_checks.py +96 -0
- proseforge_agent/install/platform_io.py +74 -0
- proseforge_agent/install/provider_setup.py +73 -0
- proseforge_agent/install/qa_matrix.py +125 -0
- proseforge_agent/install/secrets.py +81 -0
- proseforge_agent/install/shell.py +64 -0
- proseforge_agent/install/support_bundle.py +152 -0
- proseforge_agent/install/uninstall.py +60 -0
- proseforge_agent/install/windows.py +81 -0
- proseforge_agent/llm/__init__.py +55 -0
- proseforge_agent/llm/base.py +100 -0
- proseforge_agent/llm/capabilities.py +181 -0
- proseforge_agent/llm/certification.py +167 -0
- proseforge_agent/llm/docs_refresh.py +83 -0
- proseforge_agent/llm/fake.py +62 -0
- proseforge_agent/llm/http.py +121 -0
- proseforge_agent/llm/openai_compatible.py +158 -0
- proseforge_agent/llm/policies.py +69 -0
- proseforge_agent/llm/probes.py +163 -0
- proseforge_agent/llm/profiles.py +108 -0
- proseforge_agent/llm/providers/__init__.py +6 -0
- proseforge_agent/llm/providers/_openai_shape.py +130 -0
- proseforge_agent/llm/providers/anthropic.py +230 -0
- proseforge_agent/llm/providers/deepseek.py +264 -0
- proseforge_agent/llm/providers/doubao.py +258 -0
- proseforge_agent/llm/providers/gemini.py +303 -0
- proseforge_agent/llm/providers/glm.py +245 -0
- proseforge_agent/llm/providers/grok.py +255 -0
- proseforge_agent/llm/providers/mimo.py +254 -0
- proseforge_agent/llm/providers/minimax.py +262 -0
- proseforge_agent/llm/providers/openai.py +261 -0
- proseforge_agent/llm/providers/profiles/anthropic.yaml +21 -0
- proseforge_agent/llm/providers/profiles/deepseek.yaml +22 -0
- proseforge_agent/llm/providers/profiles/doubao.yaml +37 -0
- proseforge_agent/llm/providers/profiles/gemini.yaml +36 -0
- proseforge_agent/llm/providers/profiles/glm.yaml +36 -0
- proseforge_agent/llm/providers/profiles/mimo.yaml +23 -0
- proseforge_agent/llm/providers/profiles/minimax.yaml +22 -0
- proseforge_agent/llm/providers/profiles/openai.yaml +36 -0
- proseforge_agent/llm/providers/profiles/qwen.yaml +22 -0
- proseforge_agent/llm/providers/profiles/xai.yaml +21 -0
- proseforge_agent/llm/providers/qwen.py +270 -0
- proseforge_agent/llm/registry.py +230 -0
- proseforge_agent/llm/router.py +329 -0
- proseforge_agent/llm/streaming.py +81 -0
- proseforge_agent/llm/usage.py +261 -0
- proseforge_agent/mcp/__init__.py +50 -0
- proseforge_agent/mcp/approval.py +180 -0
- proseforge_agent/mcp/client.py +600 -0
- proseforge_agent/mcp/credentials.py +44 -0
- proseforge_agent/mcp/policy.py +146 -0
- proseforge_agent/mcp/registry.py +145 -0
- proseforge_agent/mcp/schema.py +179 -0
- proseforge_agent/memory/__init__.py +18 -0
- proseforge_agent/memory/compact.py +87 -0
- proseforge_agent/memory/ingest.py +79 -0
- proseforge_agent/memory/nudges.py +61 -0
- proseforge_agent/memory/review.py +66 -0
- proseforge_agent/memory/schema.py +62 -0
- proseforge_agent/memory/store.py +254 -0
- proseforge_agent/memory/user_model.py +125 -0
- proseforge_agent/notifications/__init__.py +24 -0
- proseforge_agent/notifications/core.py +98 -0
- proseforge_agent/notifications/desktop.py +55 -0
- proseforge_agent/notifications/jobs.py +171 -0
- proseforge_agent/notifications/webhook.py +97 -0
- proseforge_agent/novel/__init__.py +141 -0
- proseforge_agent/novel/approval_queue.py +153 -0
- proseforge_agent/novel/artifacts.py +107 -0
- proseforge_agent/novel/backup_verification.py +197 -0
- proseforge_agent/novel/bible.py +102 -0
- proseforge_agent/novel/character_arcs.py +153 -0
- proseforge_agent/novel/continuity.py +160 -0
- proseforge_agent/novel/draft_versioning.py +242 -0
- proseforge_agent/novel/editorial_pipeline.py +200 -0
- proseforge_agent/novel/exporter.py +160 -0
- proseforge_agent/novel/foreshadowing.py +147 -0
- proseforge_agent/novel/importer.py +241 -0
- proseforge_agent/novel/literary_regression.py +154 -0
- proseforge_agent/novel/manifest.py +110 -0
- proseforge_agent/novel/manuscript_search.py +105 -0
- proseforge_agent/novel/plot_threads.py +124 -0
- proseforge_agent/novel/project_health.py +156 -0
- proseforge_agent/novel/publishing.py +81 -0
- proseforge_agent/novel/reader_review.py +276 -0
- proseforge_agent/novel/relationship_graph.py +138 -0
- proseforge_agent/novel/reorganize.py +149 -0
- proseforge_agent/novel/rewrite_strategies.py +153 -0
- proseforge_agent/novel/safety.py +84 -0
- proseforge_agent/novel/scenes.py +215 -0
- proseforge_agent/novel/storage.py +69 -0
- proseforge_agent/novel/style_profile.py +184 -0
- proseforge_agent/novel/timeline.py +186 -0
- proseforge_agent/novel/writing_analytics.py +165 -0
- proseforge_agent/novel/writing_quality.py +213 -0
- proseforge_agent/novel/writing_rules.py +119 -0
- proseforge_agent/planning/__init__.py +26 -0
- proseforge_agent/planning/intake.py +69 -0
- proseforge_agent/planning/phase_plan.py +210 -0
- proseforge_agent/plugins/__init__.py +41 -0
- proseforge_agent/plugins/dependencies.py +143 -0
- proseforge_agent/plugins/discovery.py +91 -0
- proseforge_agent/plugins/harness.py +151 -0
- proseforge_agent/plugins/hooks.py +120 -0
- proseforge_agent/plugins/manager.py +118 -0
- proseforge_agent/plugins/manifest.py +72 -0
- proseforge_agent/plugins/permissions.py +60 -0
- proseforge_agent/plugins/sandbox.py +109 -0
- proseforge_agent/proseforge/__init__.py +11 -0
- proseforge_agent/proseforge/adapter.py +158 -0
- proseforge_agent/proseforge/results.py +45 -0
- proseforge_agent/release/__init__.py +151 -0
- proseforge_agent/release/complete_agent_gate.py +98 -0
- proseforge_agent/release/publish.py +227 -0
- proseforge_agent/release/version_policy.py +98 -0
- proseforge_agent/reports/__init__.py +29 -0
- proseforge_agent/reports/registry.py +68 -0
- proseforge_agent/reports/render.py +100 -0
- proseforge_agent/retrieval/__init__.py +45 -0
- proseforge_agent/retrieval/embeddings.py +70 -0
- proseforge_agent/retrieval/evaluation.py +96 -0
- proseforge_agent/retrieval/evidence.py +207 -0
- proseforge_agent/retrieval/hybrid.py +141 -0
- proseforge_agent/retrieval/index.py +65 -0
- proseforge_agent/retrieval/ingestion.py +216 -0
- proseforge_agent/retrieval/router.py +82 -0
- proseforge_agent/retrieval/vector_store.py +199 -0
- proseforge_agent/service/__init__.py +6 -0
- proseforge_agent/service/api.py +111 -0
- proseforge_agent/service/http_server.py +163 -0
- proseforge_agent/setup/__init__.py +27 -0
- proseforge_agent/setup/config_generator.py +271 -0
- proseforge_agent/setup/first_run.py +106 -0
- proseforge_agent/setup/modes.py +144 -0
- proseforge_agent/setup/recovery.py +45 -0
- proseforge_agent/setup/summary.py +25 -0
- proseforge_agent/setup/wizard.py +212 -0
- proseforge_agent/skills/__init__.py +29 -0
- proseforge_agent/skills/audit.py +45 -0
- proseforge_agent/skills/creation.py +159 -0
- proseforge_agent/skills/hub.py +102 -0
- proseforge_agent/skills/improvement.py +148 -0
- proseforge_agent/skills/install.py +157 -0
- proseforge_agent/skills/registry.py +134 -0
- proseforge_agent/skills/usage.py +112 -0
- proseforge_agent/testing/__init__.py +23 -0
- proseforge_agent/testing/fakes.py +140 -0
- proseforge_agent/tools/__init__.py +17 -0
- proseforge_agent/tools/managed/__init__.py +218 -0
- proseforge_agent/tools/managed/cloud_browser.py +174 -0
- proseforge_agent/tools/managed/media.py +130 -0
- proseforge_agent/tools/managed/url_safety.py +105 -0
- proseforge_agent/tools/managed/web_search.py +101 -0
- proseforge_agent/tui/__init__.py +7 -0
- proseforge_agent/tui/ansi.py +72 -0
- proseforge_agent/tui/app.py +367 -0
- proseforge_agent/tui/fullscreen.py +339 -0
- proseforge_agent/tui/keys.py +180 -0
- proseforge_agent/tui/screen.py +111 -0
- proseforge_agent/tui/streaming.py +83 -0
- proseforge_agent/workflow/__init__.py +29 -0
- proseforge_agent/workflow/recovery.py +68 -0
- proseforge_agent/workflow/state.py +289 -0
- proseforge_agent/workspace.py +107 -0
- proseforge_agent-0.2.0.dist-info/METADATA +587 -0
- proseforge_agent-0.2.0.dist-info/RECORD +284 -0
- proseforge_agent-0.2.0.dist-info/WHEEL +5 -0
- proseforge_agent-0.2.0.dist-info/entry_points.txt +2 -0
- proseforge_agent-0.2.0.dist-info/licenses/LICENSE +201 -0
- proseforge_agent-0.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
"""Agent audit trail recording, export, and replay."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import re
|
|
7
|
+
from dataclasses import asdict, dataclass, field
|
|
8
|
+
from datetime import datetime, timezone
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from ..chat.transcript import append_jsonl, read_jsonl
|
|
13
|
+
from ..errors import ConfigurationError
|
|
14
|
+
|
|
15
|
+
UTC = timezone.utc
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
_SENSITIVE_KEY_EXACT = {"api_key", "apikey", "token", "secret", "password", "authorization"}
|
|
19
|
+
_ASSIGNMENT_RE = re.compile(r"(?i)\b(api[_-]?key|token|secret|password|authorization)=\S+")
|
|
20
|
+
_SECRET_TOKEN_RE = re.compile(r"\b(?:sk|tok)-[A-Za-z0-9_-]+")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass(frozen=True)
|
|
24
|
+
class AuditStep:
|
|
25
|
+
"""One recorded agent decision step."""
|
|
26
|
+
|
|
27
|
+
session_id: str
|
|
28
|
+
step: int
|
|
29
|
+
input: str = ""
|
|
30
|
+
intent: dict[str, Any] = field(default_factory=dict)
|
|
31
|
+
system_prompt_version: str = ""
|
|
32
|
+
evidence_pack: list[dict[str, Any]] = field(default_factory=list)
|
|
33
|
+
tool_choice: str = ""
|
|
34
|
+
tool_args: dict[str, Any] = field(default_factory=dict)
|
|
35
|
+
tool_result: dict[str, Any] = field(default_factory=dict)
|
|
36
|
+
provider: dict[str, Any] = field(default_factory=dict)
|
|
37
|
+
latency_ms: int = 0
|
|
38
|
+
token_usage: dict[str, Any] = field(default_factory=dict)
|
|
39
|
+
model_output: str = ""
|
|
40
|
+
final_action: str = ""
|
|
41
|
+
trace_id: str = ""
|
|
42
|
+
created_at: str = ""
|
|
43
|
+
|
|
44
|
+
def to_dict(self) -> dict[str, Any]:
|
|
45
|
+
return asdict(self)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass(frozen=True)
|
|
49
|
+
class ReplayResult:
|
|
50
|
+
"""Deterministic replay summary from an audit session."""
|
|
51
|
+
|
|
52
|
+
session_id: str
|
|
53
|
+
step_count: int
|
|
54
|
+
final_output: str
|
|
55
|
+
actions: list[str]
|
|
56
|
+
|
|
57
|
+
def to_dict(self) -> dict[str, Any]:
|
|
58
|
+
return asdict(self)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class AuditTrailStore:
|
|
62
|
+
"""Append-only per-session audit log."""
|
|
63
|
+
|
|
64
|
+
def __init__(self, root: str | Path) -> None:
|
|
65
|
+
self.root = Path(root)
|
|
66
|
+
|
|
67
|
+
def record_turn(self, session_id: str, payload: dict[str, Any]) -> AuditStep:
|
|
68
|
+
existing, _warnings = read_jsonl(self._path(session_id))
|
|
69
|
+
sanitized = _redact(payload)
|
|
70
|
+
step = AuditStep(
|
|
71
|
+
session_id=session_id,
|
|
72
|
+
step=len(existing) + 1,
|
|
73
|
+
input=str(sanitized.get("input", "")),
|
|
74
|
+
intent=dict(sanitized.get("intent") or {}),
|
|
75
|
+
system_prompt_version=str(sanitized.get("system_prompt_version", "")),
|
|
76
|
+
evidence_pack=list(sanitized.get("evidence_pack") or []),
|
|
77
|
+
tool_choice=str(sanitized.get("tool_choice", "")),
|
|
78
|
+
tool_args=dict(sanitized.get("tool_args") or {}),
|
|
79
|
+
tool_result=dict(sanitized.get("tool_result") or {}),
|
|
80
|
+
provider=dict(sanitized.get("provider") or {}),
|
|
81
|
+
latency_ms=int(sanitized.get("latency_ms") or 0),
|
|
82
|
+
token_usage=dict(sanitized.get("token_usage") or {}),
|
|
83
|
+
model_output=str(sanitized.get("model_output", "")),
|
|
84
|
+
final_action=str(sanitized.get("final_action", "")),
|
|
85
|
+
trace_id=str(sanitized.get("trace_id", "")),
|
|
86
|
+
created_at=str(sanitized.get("created_at") or datetime.now(UTC).isoformat()),
|
|
87
|
+
)
|
|
88
|
+
append_jsonl(self._path(session_id), step.to_dict())
|
|
89
|
+
return step
|
|
90
|
+
|
|
91
|
+
def list_session(self, session_id: str) -> list[AuditStep]:
|
|
92
|
+
rows, warnings = read_jsonl(self._path(session_id))
|
|
93
|
+
if warnings:
|
|
94
|
+
raise ConfigurationError("; ".join(warnings))
|
|
95
|
+
return [_step_from_dict(row) for row in rows]
|
|
96
|
+
|
|
97
|
+
def get_step(self, session_id: str, step: int) -> AuditStep:
|
|
98
|
+
for record in self.list_session(session_id):
|
|
99
|
+
if record.step == step:
|
|
100
|
+
return record
|
|
101
|
+
raise ConfigurationError(f"audit step {step} not found for session {session_id!r}")
|
|
102
|
+
|
|
103
|
+
def export_json(self, session_id: str) -> str:
|
|
104
|
+
return json.dumps(
|
|
105
|
+
[step.to_dict() for step in self.list_session(session_id)],
|
|
106
|
+
ensure_ascii=False,
|
|
107
|
+
indent=2,
|
|
108
|
+
sort_keys=True,
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
def export_markdown(self, session_id: str) -> str:
|
|
112
|
+
lines = [f"# Audit Session {session_id}", ""]
|
|
113
|
+
for step in self.list_session(session_id):
|
|
114
|
+
lines.extend(
|
|
115
|
+
[
|
|
116
|
+
f"## Step {step.step}",
|
|
117
|
+
"",
|
|
118
|
+
f"- intent: {step.intent.get('name', '')}",
|
|
119
|
+
f"- provider: {step.provider.get('name', '')}",
|
|
120
|
+
f"- prompt: {step.system_prompt_version or '(unknown)'}",
|
|
121
|
+
f"- final_action: {step.final_action or '(unknown)'}",
|
|
122
|
+
"",
|
|
123
|
+
"### Input",
|
|
124
|
+
step.input or "(empty)",
|
|
125
|
+
"",
|
|
126
|
+
"### Output",
|
|
127
|
+
step.model_output or "(empty)",
|
|
128
|
+
"",
|
|
129
|
+
]
|
|
130
|
+
)
|
|
131
|
+
return "\n".join(lines).rstrip() + "\n"
|
|
132
|
+
|
|
133
|
+
def replay(self, session_id: str) -> ReplayResult:
|
|
134
|
+
steps = self.list_session(session_id)
|
|
135
|
+
final_output = steps[-1].model_output if steps else ""
|
|
136
|
+
actions = [step.final_action or step.intent.get("name", "") for step in steps]
|
|
137
|
+
return ReplayResult(
|
|
138
|
+
session_id=session_id,
|
|
139
|
+
step_count=len(steps),
|
|
140
|
+
final_output=final_output,
|
|
141
|
+
actions=actions,
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
def _path(self, session_id: str) -> Path:
|
|
145
|
+
return self.root / "audit" / f"{session_id}.jsonl"
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _step_from_dict(payload: dict[str, Any]) -> AuditStep:
|
|
149
|
+
return AuditStep(
|
|
150
|
+
session_id=str(payload.get("session_id", "")),
|
|
151
|
+
step=int(payload.get("step") or 0),
|
|
152
|
+
input=str(payload.get("input", "")),
|
|
153
|
+
intent=dict(payload.get("intent") or {}),
|
|
154
|
+
system_prompt_version=str(payload.get("system_prompt_version", "")),
|
|
155
|
+
evidence_pack=list(payload.get("evidence_pack") or []),
|
|
156
|
+
tool_choice=str(payload.get("tool_choice", "")),
|
|
157
|
+
tool_args=dict(payload.get("tool_args") or {}),
|
|
158
|
+
tool_result=dict(payload.get("tool_result") or {}),
|
|
159
|
+
provider=dict(payload.get("provider") or {}),
|
|
160
|
+
latency_ms=int(payload.get("latency_ms") or 0),
|
|
161
|
+
token_usage=dict(payload.get("token_usage") or {}),
|
|
162
|
+
model_output=str(payload.get("model_output", "")),
|
|
163
|
+
final_action=str(payload.get("final_action", "")),
|
|
164
|
+
trace_id=str(payload.get("trace_id", "")),
|
|
165
|
+
created_at=str(payload.get("created_at", "")),
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _redact(value: Any) -> Any:
|
|
170
|
+
if isinstance(value, dict):
|
|
171
|
+
redacted: dict[str, Any] = {}
|
|
172
|
+
for key, item in value.items():
|
|
173
|
+
lowered = str(key).lower()
|
|
174
|
+
if _is_sensitive_key(lowered):
|
|
175
|
+
redacted[key] = "[redacted]"
|
|
176
|
+
else:
|
|
177
|
+
redacted[key] = _redact(item)
|
|
178
|
+
return redacted
|
|
179
|
+
if isinstance(value, list):
|
|
180
|
+
return [_redact(item) for item in value]
|
|
181
|
+
if isinstance(value, str):
|
|
182
|
+
return _SECRET_TOKEN_RE.sub("[redacted]", _ASSIGNMENT_RE.sub(lambda match: f"{match.group(1)}=[redacted]", value))
|
|
183
|
+
return value
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _is_sensitive_key(key: str) -> bool:
|
|
187
|
+
return (
|
|
188
|
+
key in _SENSITIVE_KEY_EXACT
|
|
189
|
+
or key.endswith("_token")
|
|
190
|
+
or key.endswith("-token")
|
|
191
|
+
or key.endswith("_secret")
|
|
192
|
+
or key.endswith("-secret")
|
|
193
|
+
or key.endswith("_password")
|
|
194
|
+
or key.endswith("-password")
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
__all__ = ["AuditStep", "AuditTrailStore", "ReplayResult"]
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""Context window token budgeting and compaction."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import asdict, dataclass, field
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from ..llm import Message
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
DEFAULT_PROVIDER_LIMITS = {
|
|
12
|
+
"fake": 4096,
|
|
13
|
+
"openai": 128000,
|
|
14
|
+
"anthropic": 200000,
|
|
15
|
+
"gemini": 1000000,
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass(frozen=True)
|
|
20
|
+
class ContextUsageReport:
|
|
21
|
+
"""Token budget report before constructing a provider prompt."""
|
|
22
|
+
|
|
23
|
+
provider: str
|
|
24
|
+
max_context_tokens: int
|
|
25
|
+
prompt_tokens: int
|
|
26
|
+
evidence_tokens: int
|
|
27
|
+
reserve_tokens: int
|
|
28
|
+
evidence_budget_tokens: int
|
|
29
|
+
remaining_tokens: int
|
|
30
|
+
truncated_evidence_ids: list[str] = field(default_factory=list)
|
|
31
|
+
|
|
32
|
+
def to_dict(self) -> dict[str, Any]:
|
|
33
|
+
return asdict(self)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class ContextWindowManager:
|
|
37
|
+
"""Estimate prompt usage, truncate evidence, and summarize old messages."""
|
|
38
|
+
|
|
39
|
+
def __init__(self, provider_limits: dict[str, int] | None = None) -> None:
|
|
40
|
+
self.provider_limits = {**DEFAULT_PROVIDER_LIMITS, **dict(provider_limits or {})}
|
|
41
|
+
|
|
42
|
+
def estimate_tokens(self, text: str) -> int:
|
|
43
|
+
return len(str(text).split())
|
|
44
|
+
|
|
45
|
+
def status(
|
|
46
|
+
self,
|
|
47
|
+
*,
|
|
48
|
+
provider: str,
|
|
49
|
+
messages: list[Message],
|
|
50
|
+
evidence: list[dict[str, Any]] | None = None,
|
|
51
|
+
reserve_tokens: int = 256,
|
|
52
|
+
max_context_tokens: int | None = None,
|
|
53
|
+
) -> ContextUsageReport:
|
|
54
|
+
max_tokens = max_context_tokens or self.provider_limits.get(provider, DEFAULT_PROVIDER_LIMITS["fake"])
|
|
55
|
+
prompt_tokens = sum(self.estimate_tokens(message.content) for message in messages)
|
|
56
|
+
evidence_budget = max(0, max_tokens - prompt_tokens - reserve_tokens)
|
|
57
|
+
included_evidence = 0
|
|
58
|
+
truncated: list[str] = []
|
|
59
|
+
for item in evidence or []:
|
|
60
|
+
item_tokens = self.estimate_tokens(str(item.get("text", "")))
|
|
61
|
+
if included_evidence + item_tokens > evidence_budget:
|
|
62
|
+
truncated.append(str(item.get("id") or "evidence"))
|
|
63
|
+
continue
|
|
64
|
+
included_evidence += item_tokens
|
|
65
|
+
remaining = max(0, max_tokens - prompt_tokens - included_evidence - reserve_tokens)
|
|
66
|
+
return ContextUsageReport(
|
|
67
|
+
provider=provider,
|
|
68
|
+
max_context_tokens=max_tokens,
|
|
69
|
+
prompt_tokens=prompt_tokens,
|
|
70
|
+
evidence_tokens=included_evidence,
|
|
71
|
+
reserve_tokens=reserve_tokens,
|
|
72
|
+
evidence_budget_tokens=evidence_budget,
|
|
73
|
+
remaining_tokens=remaining,
|
|
74
|
+
truncated_evidence_ids=truncated,
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
def compact_messages(self, messages: list[Message], *, keep_last: int = 6) -> list[Message]:
|
|
78
|
+
if keep_last < 0:
|
|
79
|
+
keep_last = 0
|
|
80
|
+
if len(messages) <= keep_last:
|
|
81
|
+
return list(messages)
|
|
82
|
+
early = messages[: len(messages) - keep_last]
|
|
83
|
+
tail = messages[-keep_last:] if keep_last else []
|
|
84
|
+
summary_text = "summary: " + " | ".join(
|
|
85
|
+
f"{message.role}: {message.content}" for message in early
|
|
86
|
+
)
|
|
87
|
+
return [Message(role="system", content=summary_text), *tail]
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
__all__ = ["ContextUsageReport", "ContextWindowManager", "DEFAULT_PROVIDER_LIMITS"]
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""Cooperative control signals for long-running agent loops."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import uuid
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass(frozen=True)
|
|
10
|
+
class ControlSignal:
|
|
11
|
+
"""One cooperative control signal consumed at a safe point."""
|
|
12
|
+
|
|
13
|
+
kind: str
|
|
14
|
+
instruction: str = ""
|
|
15
|
+
reason: str = ""
|
|
16
|
+
trace_id: str = ""
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ControlToken:
|
|
20
|
+
"""Thread-safe enough single-process token for interrupt and steer requests."""
|
|
21
|
+
|
|
22
|
+
def __init__(self) -> None:
|
|
23
|
+
self._signal: ControlSignal | None = None
|
|
24
|
+
|
|
25
|
+
@staticmethod
|
|
26
|
+
def interrupt_signal(reason: str = "") -> ControlSignal:
|
|
27
|
+
return ControlSignal(kind="interrupt", reason=reason, trace_id=f"control-{uuid.uuid4().hex[:12]}")
|
|
28
|
+
|
|
29
|
+
@staticmethod
|
|
30
|
+
def steer_signal(instruction: str) -> ControlSignal:
|
|
31
|
+
return ControlSignal(kind="steer", instruction=instruction, trace_id=f"control-{uuid.uuid4().hex[:12]}")
|
|
32
|
+
|
|
33
|
+
def interrupt(self, reason: str = "user requested stop") -> None:
|
|
34
|
+
self._signal = self.interrupt_signal(reason)
|
|
35
|
+
|
|
36
|
+
def steer(self, instruction: str) -> None:
|
|
37
|
+
self._signal = self.steer_signal(instruction)
|
|
38
|
+
|
|
39
|
+
def poll(self) -> ControlSignal | None:
|
|
40
|
+
signal = self._signal
|
|
41
|
+
self._signal = None
|
|
42
|
+
return signal
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
__all__ = ["ControlSignal", "ControlToken"]
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"""Graceful feature degradation and runtime capability reports."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import asdict, dataclass, field
|
|
6
|
+
from enum import IntEnum
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class FeatureLevel(IntEnum):
|
|
11
|
+
"""Ordered runtime capability levels."""
|
|
12
|
+
|
|
13
|
+
OFFLINE = 0
|
|
14
|
+
LOCAL_MODEL = 1
|
|
15
|
+
REMOTE_PROVIDER = 2
|
|
16
|
+
|
|
17
|
+
@property
|
|
18
|
+
def label(self) -> str:
|
|
19
|
+
return {
|
|
20
|
+
FeatureLevel.OFFLINE: "offline",
|
|
21
|
+
FeatureLevel.LOCAL_MODEL: "local_model",
|
|
22
|
+
FeatureLevel.REMOTE_PROVIDER: "remote_provider",
|
|
23
|
+
}[self]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass(frozen=True)
|
|
27
|
+
class FeatureDeclaration:
|
|
28
|
+
"""Capability requirement for a feature."""
|
|
29
|
+
|
|
30
|
+
id: str
|
|
31
|
+
required_level: FeatureLevel
|
|
32
|
+
dependencies: list[str] = field(default_factory=list)
|
|
33
|
+
guidance: str = ""
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass(frozen=True)
|
|
37
|
+
class FeatureCheck:
|
|
38
|
+
"""Decision for one feature under current runtime capabilities."""
|
|
39
|
+
|
|
40
|
+
feature_id: str
|
|
41
|
+
allowed: bool
|
|
42
|
+
status: str
|
|
43
|
+
required_level: str
|
|
44
|
+
available_level: str
|
|
45
|
+
guidance: str = ""
|
|
46
|
+
|
|
47
|
+
def to_dict(self) -> dict[str, Any]:
|
|
48
|
+
return asdict(self)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass(frozen=True)
|
|
52
|
+
class CapabilityReport:
|
|
53
|
+
"""Runtime capability report for all declared features."""
|
|
54
|
+
|
|
55
|
+
available_level: str
|
|
56
|
+
features: dict[str, dict[str, Any]]
|
|
57
|
+
|
|
58
|
+
def to_dict(self) -> dict[str, Any]:
|
|
59
|
+
return asdict(self)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
DEFAULT_FEATURES = {
|
|
63
|
+
"export_txt": FeatureDeclaration(
|
|
64
|
+
id="export_txt",
|
|
65
|
+
required_level=FeatureLevel.OFFLINE,
|
|
66
|
+
guidance="TXT/Markdown export can run offline.",
|
|
67
|
+
),
|
|
68
|
+
"manifest_validate": FeatureDeclaration(
|
|
69
|
+
id="manifest_validate",
|
|
70
|
+
required_level=FeatureLevel.OFFLINE,
|
|
71
|
+
guidance="Manifest validation can run offline.",
|
|
72
|
+
),
|
|
73
|
+
"keyword_search": FeatureDeclaration(
|
|
74
|
+
id="keyword_search",
|
|
75
|
+
required_level=FeatureLevel.OFFLINE,
|
|
76
|
+
guidance="Keyword search can run from local text indexes.",
|
|
77
|
+
),
|
|
78
|
+
"stats": FeatureDeclaration(
|
|
79
|
+
id="stats",
|
|
80
|
+
required_level=FeatureLevel.OFFLINE,
|
|
81
|
+
guidance="Writing stats can run from local project files.",
|
|
82
|
+
),
|
|
83
|
+
"backup": FeatureDeclaration(
|
|
84
|
+
id="backup",
|
|
85
|
+
required_level=FeatureLevel.OFFLINE,
|
|
86
|
+
guidance="Backups can run without provider access.",
|
|
87
|
+
),
|
|
88
|
+
"fake_chat": FeatureDeclaration(
|
|
89
|
+
id="fake_chat",
|
|
90
|
+
required_level=FeatureLevel.OFFLINE,
|
|
91
|
+
guidance="Fake provider chat can run offline.",
|
|
92
|
+
),
|
|
93
|
+
"rewrite": FeatureDeclaration(
|
|
94
|
+
id="rewrite",
|
|
95
|
+
required_level=FeatureLevel.LOCAL_MODEL,
|
|
96
|
+
dependencies=["provider"],
|
|
97
|
+
guidance="Rewrite requires provider capability; use fake chat or export while offline.",
|
|
98
|
+
),
|
|
99
|
+
"cloud_sync": FeatureDeclaration(
|
|
100
|
+
id="cloud_sync",
|
|
101
|
+
required_level=FeatureLevel.REMOTE_PROVIDER,
|
|
102
|
+
dependencies=["network"],
|
|
103
|
+
guidance="Cloud sync requires network connectivity.",
|
|
104
|
+
),
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class CapabilityRuntime:
|
|
109
|
+
"""Evaluate features against the current runtime capability level."""
|
|
110
|
+
|
|
111
|
+
def __init__(
|
|
112
|
+
self,
|
|
113
|
+
*,
|
|
114
|
+
available_level: FeatureLevel = FeatureLevel.OFFLINE,
|
|
115
|
+
dependencies: dict[str, bool] | None = None,
|
|
116
|
+
declarations: dict[str, FeatureDeclaration] | None = None,
|
|
117
|
+
) -> None:
|
|
118
|
+
self.available_level = available_level
|
|
119
|
+
self.dependencies = dependencies or {}
|
|
120
|
+
self.declarations = declarations or DEFAULT_FEATURES
|
|
121
|
+
|
|
122
|
+
def check(self, feature_id: str) -> FeatureCheck:
|
|
123
|
+
declaration = self.declarations[feature_id]
|
|
124
|
+
missing = [name for name in declaration.dependencies if not self.dependencies.get(name, False)]
|
|
125
|
+
if self.available_level < declaration.required_level:
|
|
126
|
+
return FeatureCheck(
|
|
127
|
+
feature_id=feature_id,
|
|
128
|
+
allowed=False,
|
|
129
|
+
status="degraded",
|
|
130
|
+
required_level=declaration.required_level.label,
|
|
131
|
+
available_level=self.available_level.label,
|
|
132
|
+
guidance=declaration.guidance,
|
|
133
|
+
)
|
|
134
|
+
if missing:
|
|
135
|
+
return FeatureCheck(
|
|
136
|
+
feature_id=feature_id,
|
|
137
|
+
allowed=False,
|
|
138
|
+
status="degraded",
|
|
139
|
+
required_level=declaration.required_level.label,
|
|
140
|
+
available_level=self.available_level.label,
|
|
141
|
+
guidance=f"requires {', '.join(missing)}; {declaration.guidance}",
|
|
142
|
+
)
|
|
143
|
+
return FeatureCheck(
|
|
144
|
+
feature_id=feature_id,
|
|
145
|
+
allowed=True,
|
|
146
|
+
status="ok",
|
|
147
|
+
required_level=declaration.required_level.label,
|
|
148
|
+
available_level=self.available_level.label,
|
|
149
|
+
guidance=declaration.guidance,
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
def report(self) -> CapabilityReport:
|
|
153
|
+
return CapabilityReport(
|
|
154
|
+
available_level=self.available_level.label,
|
|
155
|
+
features={feature_id: self.check(feature_id).to_dict() for feature_id in sorted(self.declarations)},
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
__all__ = [
|
|
160
|
+
"CapabilityReport",
|
|
161
|
+
"CapabilityRuntime",
|
|
162
|
+
"DEFAULT_FEATURES",
|
|
163
|
+
"FeatureCheck",
|
|
164
|
+
"FeatureDeclaration",
|
|
165
|
+
"FeatureLevel",
|
|
166
|
+
]
|