agentpace 0.9.2__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.
agentpace/pseudonym.py ADDED
@@ -0,0 +1,51 @@
1
+ """Pseudonyms derived from a random local secret.
2
+
3
+ Nothing about a person goes in: the contributor hash is a fixed label under the secret, so there
4
+ is no email or handle for a dictionary attack to guess. The secret never leaves this machine.
5
+ Deleting it makes you a new contributor.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import hashlib
11
+ import hmac
12
+ import os
13
+ import secrets
14
+ from pathlib import Path
15
+
16
+ _SECRET_BYTES = 32
17
+
18
+
19
+ def load_secret(home: Path) -> bytes:
20
+ """Read the local secret, creating it on first use with owner-only permissions."""
21
+ path = home / "secret"
22
+ home.mkdir(parents=True, exist_ok=True)
23
+ try:
24
+ descriptor = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
25
+ except FileExistsError:
26
+ return bytes.fromhex(path.read_text(encoding="ascii").strip())
27
+ secret = secrets.token_bytes(_SECRET_BYTES)
28
+ with os.fdopen(descriptor, "w", encoding="ascii") as handle:
29
+ handle.write(secret.hex())
30
+ return secret
31
+
32
+
33
+ def _digest(secret: bytes, message: str, length: int) -> str:
34
+ return hmac.new(secret, message.encode("utf-8"), hashlib.sha256).hexdigest()[:length]
35
+
36
+
37
+ def contributor(secret: bytes) -> str:
38
+ return _digest(secret, "contributor-v1", 16)
39
+
40
+
41
+ def batch(secret: bytes, session: str) -> str:
42
+ return _digest(secret, f"batch-v1:{session}", 12)
43
+
44
+
45
+ def turn_session(secret: bytes, session: str) -> str:
46
+ """A separate label from `batch`, so a turn could never be joined to a run row by session."""
47
+ return _digest(secret, f"turn-v1:{session}", 12)
48
+
49
+
50
+ def project(secret: bytes, folder: str) -> str:
51
+ return _digest(secret, f"project-v1:{folder}", 12)
agentpace/py.typed ADDED
File without changes
agentpace/render.py ADDED
@@ -0,0 +1,152 @@
1
+ """Plain-text and JSON views of results. No math here."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import dataclasses
6
+ import json
7
+ from typing import Any
8
+
9
+ from agentpace.estimate import HumanPart, PlanRange
10
+
11
+ HUMAN_PART = "Human part (review, testing, decisions, waiting between agent turns)"
12
+ CLOCK_NOTE = (
13
+ "The session clock counts agent runs and the gaps between them, each gap up to the idle cap "
14
+ "(900 s by default), so waiting on you between runs sits inside the total up to that cap."
15
+ )
16
+
17
+
18
+ def human_text(human: HumanPart) -> str:
19
+ """The human block. It names the quantity and its status, and never offers a number derived
20
+ from agent time. A measured form quotes only the user's own waits, per human turn. No
21
+ allowance for the human part is added to the total, but waits up to the idle cap already sit
22
+ inside the session clock, so the measured form says not to add them."""
23
+ p50, p80, p90 = human.per_turn_p50, human.per_turn_p80, human.per_turn_p90
24
+ if p50 is not None and p80 is not None and p90 is not None:
25
+ return (
26
+ f"{HUMAN_PART}: {human.status} from {human.source}. Waiting on you per human turn: "
27
+ f"P50 {duration(p50)}, P80 {duration(p80)}, P90 {duration(p90)} "
28
+ f"({human.gaps} gaps, {human.sessions} sessions); elapsed time, not effort. "
29
+ "No allowance for it is added to the total above, and up to the idle cap such waits "
30
+ "already sit inside the session clock, so do not add them to it. "
31
+ "Do not derive it from the agent time."
32
+ )
33
+ return (
34
+ f"{HUMAN_PART}: {human.status}. No allowance for it is added to the total above. "
35
+ "State it separately; do not derive it from the agent time."
36
+ )
37
+
38
+
39
+ def duration(seconds: float) -> str:
40
+ """Two significant figures: anything finer would suggest precision the data lacks."""
41
+ minutes = seconds / 60
42
+ if seconds < 90:
43
+ return f"{seconds:.0f} s"
44
+ if minutes < 90:
45
+ return f"{minutes:.0f} min"
46
+ hours = minutes / 60
47
+ return f"{hours:.1f} h" if hours < 48 else f"{hours / 24:.1f} days"
48
+
49
+
50
+ def quote_text(quoted: str) -> str:
51
+ """A timeline someone already gave, verbatim. It is calendar time from an unknown source, so
52
+ it is never converted into agent time, compared with the range or turned into a ratio."""
53
+ return f'Quoted: "{quoted}" (calendar time, as quoted; unverified, not converted)'
54
+
55
+
56
+ def plan_text(plan: PlanRange, quoted: str | None = None) -> str:
57
+ width = max(len("task"), *(len(task.id) for task in plan.tasks))
58
+ lines = [
59
+ f"Plan total on the session clock: P50 {duration(plan.p50)}, "
60
+ f"P80 {duration(plan.p80)}, "
61
+ f"P90 {duration(plan.p90)}",
62
+ *([quote_text(quoted)] if quoted is not None else []),
63
+ CLOCK_NOTE,
64
+ human_text(plan.human),
65
+ "",
66
+ f"{'task':<{width}} {'P50':>9} {'P80':>9} {'P90':>9} {'critical':>8} cell (n)",
67
+ ]
68
+ for task in plan.tasks:
69
+ lines.append(
70
+ f"{task.id:<{width}} {duration(task.p50):>9} {duration(task.p80):>9} "
71
+ f"{duration(task.p90):>9} {task.criticality:>7.0%} {task.cell} (n={task.n})"
72
+ )
73
+ lines += [
74
+ "",
75
+ f"Critical path: {' -> '.join(plan.critical_path)} "
76
+ f"(longest in {plan.critical_path_share:.0%} of simulations)",
77
+ ]
78
+ if plan.effective_parallelism is None:
79
+ lines.append(f"Effective parallelism at cap {plan.agent_cap}: not learned yet")
80
+ else:
81
+ lines.append(
82
+ f"Effective parallelism at cap {plan.agent_cap}: {plan.effective_parallelism:.2f} "
83
+ f"(from {plan.parallelism_sessions} sessions). Throughput set the total in "
84
+ f"{plan.throughput_bound_share:.0%} of simulations."
85
+ )
86
+ excluded = f", {plan.rows_excluded} not completed so excluded" if plan.rows_excluded else ""
87
+ lines.append(
88
+ f"Method: {plan.method}. {plan.rows_used} runs{excluded}. "
89
+ f"{plan.draws} simulations, seed {plan.seed}."
90
+ )
91
+ if plan.size_used:
92
+ lines.append("Size used: oracle track, stored size is measured after a run.")
93
+ if plan.warnings:
94
+ lines += ["", "Warnings:", *(f" - {note}" for note in plan.warnings)]
95
+ return "\n".join(lines)
96
+
97
+
98
+ def plan_dict(plan: PlanRange) -> dict[str, Any]:
99
+ data = dataclasses.asdict(plan)
100
+ data["human"] = {key: value for key, value in data["human"].items() if value is not None}
101
+ return data
102
+
103
+
104
+ def plan_json(plan: PlanRange) -> str:
105
+ return json.dumps(plan_dict(plan), indent=2)
106
+
107
+
108
+ def score_text(forecast: dict[str, Any], score: dict[str, Any], source: str) -> str:
109
+ actual = score["actual_s"]
110
+ hits = ", ".join(
111
+ f"P{key[1:]} {'yes' if actual <= forecast[key] else 'no'}" for key in ("p50", "p80", "p90")
112
+ )
113
+ lines = [
114
+ f"Forecast {forecast['id']}: P50 {duration(forecast['p50'])}, "
115
+ f"P80 {duration(forecast['p80'])}, P90 {duration(forecast['p90'])}",
116
+ f"Actual: {duration(actual)} ({source})",
117
+ f"Within: {hits}",
118
+ ]
119
+ if "quoted_s" in forecast:
120
+ ratio = forecast["quoted_s"] / actual
121
+ closer = "closer" if score["quote_closer"] else "not closer"
122
+ lines.append(
123
+ f'Quoted: "{forecast["quoted"]}" (as quoted, calendar time), {ratio:.1f}x the actual, '
124
+ f"{closer} than the P50 by ratio"
125
+ )
126
+ elif "quoted" in forecast:
127
+ lines.append(f'Quoted: "{forecast["quoted"]}", not parseable, so not scored')
128
+ lines.append(f"Recorded score for {forecast['id']}.")
129
+ return "\n".join(lines)
130
+
131
+
132
+ def ledger_summary_text(summary: Any) -> str:
133
+ lines = [f"Your forecasts, n = {summary.scored}, of which {summary.attested} attested by you."]
134
+ if summary.within is None:
135
+ lines.append(f"Not enough scored forecasts (n = {summary.scored}, need 5).")
136
+ else:
137
+ within = ", ".join(f"P{key[1:]} {share:.0%}" for key, share in summary.within.items())
138
+ lines.append(f"Actual within: {within}. Mean pinball loss: {summary.pinball:.0f} s.")
139
+ if summary.quotes:
140
+ if summary.quote_closer is None:
141
+ lines.append(f"Quoted timelines: not enough to rate (n = {summary.quotes}, need 5).")
142
+ else:
143
+ lines.append(
144
+ f"Quoted timelines (n = {summary.quotes}, calendar time as quoted): closer than "
145
+ f"the P50 in {summary.quote_closer:.0%}, median {summary.quote_ratio:.1f}x the "
146
+ "actual."
147
+ )
148
+ lines.append(
149
+ "Actuals are session spans on their own clock (agent runs plus the gaps between them, "
150
+ "each gap up to the idle cap), or figures you attested, as given."
151
+ )
152
+ return "\n".join(lines)