handcode 0.3.0rc1__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 (67) hide show
  1. agentctl/__init__.py +0 -0
  2. agentctl/adapters/__init__.py +0 -0
  3. agentctl/adapters/litellm/__init__.py +9 -0
  4. agentctl/adapters/litellm/hook.py +49 -0
  5. agentctl/adapters/litellm/recorder.py +187 -0
  6. agentctl/adapters/openhands/__init__.py +169 -0
  7. agentctl/adapters/openhands/handoff.py +155 -0
  8. agentctl/adapters/openhands/seam_b.py +259 -0
  9. agentctl/adapters/openhands/seam_c.py +209 -0
  10. agentctl/cli.py +1450 -0
  11. agentctl/control/__init__.py +0 -0
  12. agentctl/control/cost/__init__.py +4 -0
  13. agentctl/control/cost/ledger.py +210 -0
  14. agentctl/control/dash.py +697 -0
  15. agentctl/control/keys.py +440 -0
  16. agentctl/control/matrix/__init__.py +0 -0
  17. agentctl/control/matrix/data/tools.yaml +149 -0
  18. agentctl/control/policy/__init__.py +10 -0
  19. agentctl/control/policy/compile.py +258 -0
  20. agentctl/control/policy/data/policy.compiled.json +38 -0
  21. agentctl/control/policy/data/policy.yaml +46 -0
  22. agentctl/control/probe.py +399 -0
  23. agentctl/control/providers.py +293 -0
  24. agentctl/control/proxy.py +536 -0
  25. agentctl/control/proxyenv.py +309 -0
  26. agentctl/control/replay/__init__.py +14 -0
  27. agentctl/control/replay/cassette.py +281 -0
  28. agentctl/control/replay/server.py +109 -0
  29. agentctl/demo/__init__.py +214 -0
  30. agentctl/demo/child.py +84 -0
  31. agentctl/demo/mock.py +79 -0
  32. agentctl/demo/tool.py +62 -0
  33. agentctl/gha.py +488 -0
  34. agentctl/kernel/__init__.py +0 -0
  35. agentctl/kernel/classify.py +170 -0
  36. agentctl/kernel/gate.py +391 -0
  37. agentctl/kernel/hook.py +229 -0
  38. agentctl/kernel/ledger/__init__.py +0 -0
  39. agentctl/kernel/ledger/models.py +160 -0
  40. agentctl/kernel/ledger/schema.sql +62 -0
  41. agentctl/kernel/ledger/store.py +596 -0
  42. agentctl/kernel/paths.py +203 -0
  43. agentctl/kernel/policy.py +160 -0
  44. agentctl/kernel/reconcile/__init__.py +31 -0
  45. agentctl/kernel/reconcile/base.py +106 -0
  46. agentctl/kernel/reconcile/external.py +137 -0
  47. agentctl/kernel/reconcile/filesystem.py +162 -0
  48. agentctl/kernel/reconcile/git.py +162 -0
  49. agentctl/runtime/__init__.py +20 -0
  50. agentctl/runtime/citations.py +179 -0
  51. agentctl/runtime/config.py +97 -0
  52. agentctl/runtime/doctor.py +335 -0
  53. agentctl/runtime/init.py +148 -0
  54. agentctl/runtime/lease.py +143 -0
  55. agentctl/runtime/orchestrate.py +187 -0
  56. agentctl/runtime/plugins.py +130 -0
  57. agentctl/runtime/report.py +361 -0
  58. agentctl/runtime/runner.py +787 -0
  59. agentctl/runtime/runs.py +191 -0
  60. agentctl/runtime/subagent.py +274 -0
  61. agentctl/runtime/tools.py +350 -0
  62. handcode-0.3.0rc1.dist-info/METADATA +659 -0
  63. handcode-0.3.0rc1.dist-info/RECORD +67 -0
  64. handcode-0.3.0rc1.dist-info/WHEEL +5 -0
  65. handcode-0.3.0rc1.dist-info/entry_points.txt +3 -0
  66. handcode-0.3.0rc1.dist-info/licenses/LICENSE +21 -0
  67. handcode-0.3.0rc1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,160 @@
1
+ """Core types for the effect ledger.
2
+
3
+ Spec: `docs/0012` §2. No I/O here, no dependencies beyond the stdlib — this
4
+ module is imported by everything in the kernel.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import hashlib
9
+ import json
10
+ from dataclasses import dataclass, field
11
+ from enum import Enum
12
+
13
+
14
+ class EffectClass(str, Enum):
15
+ """How a tool's side effect behaves under repetition.
16
+
17
+ Ordering matters: `DESTRUCTIVE` is the safe default direction, so an
18
+ unknown tool must never classify below `EXTERNAL`.
19
+ """
20
+
21
+ PURE_READ = "PURE_READ"
22
+ IDEMPOTENT_WRITE = "IDEMPOTENT_WRITE"
23
+ NON_IDEMPOTENT_WRITE = "NON_IDEMPOTENT_WRITE"
24
+ EXTERNAL = "EXTERNAL"
25
+ DESTRUCTIVE = "DESTRUCTIVE"
26
+
27
+ @property
28
+ def severity(self) -> int:
29
+ """Rank by danger. Used to resolve competing classification rules.
30
+
31
+ A command can match several rules (`ls && rm -rf x` matches both a
32
+ benign prefix and a destructive one). The classifier must take the
33
+ most dangerous match, never the first.
34
+ """
35
+ return _SEVERITY[self]
36
+
37
+ @property
38
+ def replay_safe(self) -> bool:
39
+ """Safe to run again when we cannot tell whether it already ran."""
40
+ return self in (EffectClass.PURE_READ, EffectClass.IDEMPOTENT_WRITE)
41
+
42
+ @property
43
+ def speculation_safe(self) -> bool:
44
+ """Safe to run *before* we know we want to, then discard.
45
+
46
+ Strictly stricter than `replay_safe` (`docs/0010` §6.4): an idempotent
47
+ write is safe to repeat but a discarded speculative write has still
48
+ mutated the world.
49
+ """
50
+ return self is EffectClass.PURE_READ
51
+
52
+
53
+ _SEVERITY: dict[EffectClass, int] = {
54
+ EffectClass.PURE_READ: 0,
55
+ EffectClass.IDEMPOTENT_WRITE: 1,
56
+ EffectClass.NON_IDEMPOTENT_WRITE: 2,
57
+ EffectClass.EXTERNAL: 3,
58
+ EffectClass.DESTRUCTIVE: 4,
59
+ }
60
+
61
+
62
+ class EffectState(str, Enum):
63
+ INTENT = "INTENT" # written before execution; the ambiguous state
64
+ COMMITTED = "COMMITTED" # tool returned; observation stored
65
+ #: The tool's result -- success OR reported failure -- is durably in the
66
+ #: conversation history the model reads. A later identical call is then
67
+ #: the model deciding to repeat, not the harness replaying (`docs/0045`).
68
+ OBSERVED = "OBSERVED"
69
+ FAILED = "FAILED" # provably did not land
70
+ BLOCKED = "BLOCKED" # fail-closed; awaiting a human
71
+
72
+
73
+ #: Legal transitions. Anything else is a bug and raises.
74
+ TRANSITIONS: dict[EffectState | None, set[EffectState]] = {
75
+ None: {EffectState.INTENT},
76
+ EffectState.INTENT: {
77
+ EffectState.COMMITTED, EffectState.FAILED,
78
+ EffectState.BLOCKED, EffectState.INTENT, # retry after FAILED bumps attempt
79
+ # Straight to OBSERVED in one write when the harness has already
80
+ # persisted the observation. Going through COMMITTED would open a crash
81
+ # window in which a reported failure sat as COMMITTED (`docs/0031`).
82
+ EffectState.OBSERVED,
83
+ },
84
+ EffectState.COMMITTED: {EffectState.OBSERVED},
85
+ EffectState.FAILED: {EffectState.INTENT},
86
+ EffectState.BLOCKED: {
87
+ EffectState.COMMITTED, EffectState.FAILED, EffectState.INTENT,
88
+ }, # human resolution only
89
+ EffectState.OBSERVED: set(),
90
+ }
91
+
92
+
93
+ class IllegalTransition(RuntimeError):
94
+ """Raised inside the store, never past `EffectGate.guard`."""
95
+
96
+
97
+ class Verdict(str, Enum):
98
+ EXECUTE = "EXECUTE" # run the tool
99
+ SUBSTITUTE = "SUBSTITUTE" # return the stored observation (Seam C only)
100
+ BLOCK = "BLOCK" # refuse; fail closed
101
+ ESCALATE = "ESCALATE" # refuse; a human must decide
102
+
103
+
104
+ @dataclass(frozen=True)
105
+ class ToolCall:
106
+ """One tool invocation, normalised away from any harness."""
107
+
108
+ tool_call_id: str
109
+ conversation_id: str
110
+ turn_id: str
111
+ tool_name: str
112
+ args: dict = field(default_factory=dict)
113
+
114
+ def intent_hash(self) -> str:
115
+ """Stable fingerprint of *what this call would do*.
116
+
117
+ Guards against a `tool_call_id` being reused with different arguments,
118
+ which would otherwise let the ledger substitute the wrong observation.
119
+ """
120
+ canonical = json.dumps(
121
+ {"t": self.tool_name, "a": self.args},
122
+ sort_keys=True, separators=(",", ":"), default=str,
123
+ )
124
+ return hashlib.sha256(canonical.encode()).hexdigest()
125
+
126
+
127
+ @dataclass(frozen=True)
128
+ class EffectRecord:
129
+ tool_call_id: str
130
+ conversation_id: str
131
+ turn_id: str
132
+ tool_name: str
133
+ intent_hash: str
134
+ effect_class: EffectClass
135
+ state: EffectState
136
+ fence_token: int
137
+ attempt: int
138
+ started_at: float
139
+ committed_at: float | None = None
140
+ observation: bytes | None = None
141
+ probe_verdict: str | None = None
142
+ pre_state: str | None = None
143
+ error: str | None = None
144
+ action_event_id: str | None = None
145
+
146
+
147
+ @dataclass(frozen=True)
148
+ class GateDecision:
149
+ verdict: Verdict
150
+ effect_class: EffectClass | None = None
151
+ observation: bytes | None = None
152
+ reason: str | None = None
153
+ #: For SUBSTITUTE: the record whose result is being handed back. Under
154
+ #: intent-hash aliasing it is not the caller's id, and it is the record to
155
+ #: mark OBSERVED once the substituted result reaches the model.
156
+ record_id: str | None = None
157
+
158
+ @property
159
+ def allows_execution(self) -> bool:
160
+ return self.verdict is Verdict.EXECUTE
@@ -0,0 +1,62 @@
1
+ -- Effect ledger. docs/0012 §2.1
2
+ --
3
+ -- synchronous = FULL is deliberate and non-negotiable: NORMAL can lose the
4
+ -- last commit on power failure, and that is exactly the record whose absence
5
+ -- causes a double effect.
6
+ PRAGMA journal_mode = WAL;
7
+ PRAGMA synchronous = FULL;
8
+
9
+ CREATE TABLE IF NOT EXISTS effect_record (
10
+ tool_call_id TEXT PRIMARY KEY,
11
+ conversation_id TEXT NOT NULL,
12
+ turn_id TEXT NOT NULL,
13
+ action_event_id TEXT,
14
+ tool_name TEXT NOT NULL,
15
+ intent_hash TEXT NOT NULL,
16
+ effect_class TEXT NOT NULL,
17
+ state TEXT NOT NULL,
18
+ fence_token INTEGER NOT NULL,
19
+ attempt INTEGER NOT NULL DEFAULT 1,
20
+ started_at REAL NOT NULL,
21
+ committed_at REAL,
22
+ observation BLOB,
23
+ probe_verdict TEXT,
24
+ -- World fingerprint captured BEFORE execution, so a probe can later ask
25
+ -- "did anything change?" without the tool having cooperated. docs/0016b
26
+ pre_state TEXT,
27
+ error TEXT,
28
+ CHECK (state IN ('INTENT','COMMITTED','OBSERVED','FAILED','BLOCKED')),
29
+ CHECK (effect_class IN
30
+ ('PURE_READ','IDEMPOTENT_WRITE','NON_IDEMPOTENT_WRITE','EXTERNAL','DESTRUCTIVE'))
31
+ );
32
+
33
+ CREATE INDEX IF NOT EXISTS ix_effect_conv ON effect_record(conversation_id, turn_id);
34
+
35
+ -- The recovery scan: everything needing attention after a crash.
36
+ CREATE INDEX IF NOT EXISTS ix_effect_open ON effect_record(state)
37
+ WHERE state IN ('INTENT','BLOCKED');
38
+
39
+ -- Fencing: one live writer per conversation.
40
+ CREATE TABLE IF NOT EXISTS lease (
41
+ conversation_id TEXT PRIMARY KEY,
42
+ holder TEXT NOT NULL,
43
+ fence_token INTEGER NOT NULL,
44
+ expires_at REAL NOT NULL
45
+ );
46
+
47
+ -- Decisions a human made on actions that asked first (docs/0042 I-06). With
48
+ -- no terminal to ask at, a dangerous action is queued as BLOCKED awaiting
49
+ -- approval; `agentctl approve` / `deny` records the answer here, keyed by
50
+ -- what the action IS (its intent hash), so the identical call the agent makes
51
+ -- on resume is recognised. An approval is used once.
52
+ CREATE TABLE IF NOT EXISTS approval (
53
+ conversation_id TEXT NOT NULL,
54
+ intent_hash TEXT NOT NULL,
55
+ tool_call_id TEXT,
56
+ summary TEXT,
57
+ decision TEXT NOT NULL CHECK (decision IN ('approve', 'deny')),
58
+ decided_at REAL NOT NULL,
59
+ told_at REAL,
60
+ used_at REAL,
61
+ PRIMARY KEY (conversation_id, intent_hash)
62
+ );