calcifier 0.1.1__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.
- calcifier/__init__.py +73 -0
- calcifier/agent_config.py +263 -0
- calcifier/agent_events.py +450 -0
- calcifier/agent_providers.py +342 -0
- calcifier/agents.py +227 -0
- calcifier/artifact.py +313 -0
- calcifier/atomic_io.py +31 -0
- calcifier/baseline.py +159 -0
- calcifier/budget.py +63 -0
- calcifier/calcify.py +51 -0
- calcifier/catalog.py +139 -0
- calcifier/cli.py +787 -0
- calcifier/cli_options.py +108 -0
- calcifier/clock.py +79 -0
- calcifier/compiler_activity.py +108 -0
- calcifier/compiler_resources.py +98 -0
- calcifier/composition.py +347 -0
- calcifier/diagnostics.py +172 -0
- calcifier/dry_run.py +500 -0
- calcifier/effect_methods.py +39 -0
- calcifier/exceptions.py +28 -0
- calcifier/filesystem_contract.py +88 -0
- calcifier/gate.py +996 -0
- calcifier/graph.py +112 -0
- calcifier/graph_build.py +241 -0
- calcifier/graph_edges.py +34 -0
- calcifier/hashing.py +9 -0
- calcifier/instruction_context.py +197 -0
- calcifier/json_config.py +16 -0
- calcifier/metadata.py +235 -0
- calcifier/mutations.py +318 -0
- calcifier/native_events.py +299 -0
- calcifier/output_annotations.py +149 -0
- calcifier/outputs.py +550 -0
- calcifier/parallel_contract.py +61 -0
- calcifier/path_syntax.py +30 -0
- calcifier/portable.py +373 -0
- calcifier/prepared_context.py +82 -0
- calcifier/prompts.py +370 -0
- calcifier/py.typed +0 -0
- calcifier/recipe.py +1217 -0
- calcifier/recipe_ast.py +577 -0
- calcifier/recipe_draft.py +218 -0
- calcifier/registry.py +83 -0
- calcifier/repair.py +534 -0
- calcifier/run_result.py +119 -0
- calcifier/runtime_conformance.py +135 -0
- calcifier/side_effects.py +91 -0
- calcifier/skill_frontmatter.py +89 -0
- calcifier/skill_package.py +271 -0
- calcifier/source_quality.py +392 -0
- calcifier/subprocesses.py +407 -0
- calcifier/taint.py +603 -0
- calcifier/tools.py +456 -0
- calcifier/trace-schema.json +768 -0
- calcifier/trace.py +532 -0
- calcifier/trace_export.py +203 -0
- calcifier/trace_profile.py +83 -0
- calcifier/trust.py +483 -0
- calcifier/type_check.py +105 -0
- calcifier/update.py +65 -0
- calcifier/usage.py +61 -0
- calcifier/validation.py +29 -0
- calcifier/windows_job.py +202 -0
- calcifier-0.1.1.dist-info/METADATA +127 -0
- calcifier-0.1.1.dist-info/RECORD +69 -0
- calcifier-0.1.1.dist-info/WHEEL +4 -0
- calcifier-0.1.1.dist-info/entry_points.txt +2 -0
- calcifier-0.1.1.dist-info/licenses/LICENSE +21 -0
calcifier/__init__.py
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""Calcifier: typed Python recipes for LLM-assisted skills."""
|
|
2
|
+
|
|
3
|
+
from .agents import agent
|
|
4
|
+
from .budget import Budget, BudgetExceeded
|
|
5
|
+
from .composition import (
|
|
6
|
+
SubrecipeError,
|
|
7
|
+
SubrecipeResult,
|
|
8
|
+
call_catalog_recipe,
|
|
9
|
+
call_recipe,
|
|
10
|
+
)
|
|
11
|
+
from .filesystem_contract import TransientAgentError
|
|
12
|
+
from .mutations import (
|
|
13
|
+
CommandResult,
|
|
14
|
+
ensure_directory,
|
|
15
|
+
run_command,
|
|
16
|
+
verify_artifact,
|
|
17
|
+
write_json,
|
|
18
|
+
write_text,
|
|
19
|
+
)
|
|
20
|
+
from .outputs import output_type, word_count
|
|
21
|
+
from .prepared_context import PreparedContext, prepare_context
|
|
22
|
+
from .recipe import (
|
|
23
|
+
Recipe,
|
|
24
|
+
RecipeError,
|
|
25
|
+
RecipeFailure,
|
|
26
|
+
RecipeRun,
|
|
27
|
+
emit,
|
|
28
|
+
fail,
|
|
29
|
+
load_recipe,
|
|
30
|
+
parallel,
|
|
31
|
+
recipe,
|
|
32
|
+
resource,
|
|
33
|
+
step,
|
|
34
|
+
)
|
|
35
|
+
from .side_effects import record_side_effect
|
|
36
|
+
from .tools import tool
|
|
37
|
+
|
|
38
|
+
__version__ = "0.1.1"
|
|
39
|
+
|
|
40
|
+
__all__ = [
|
|
41
|
+
"Budget",
|
|
42
|
+
"BudgetExceeded",
|
|
43
|
+
"CommandResult",
|
|
44
|
+
"PreparedContext",
|
|
45
|
+
"Recipe",
|
|
46
|
+
"RecipeError",
|
|
47
|
+
"RecipeFailure",
|
|
48
|
+
"RecipeRun",
|
|
49
|
+
"SubrecipeError",
|
|
50
|
+
"SubrecipeResult",
|
|
51
|
+
"TransientAgentError",
|
|
52
|
+
"__version__",
|
|
53
|
+
"agent",
|
|
54
|
+
"call_catalog_recipe",
|
|
55
|
+
"call_recipe",
|
|
56
|
+
"emit",
|
|
57
|
+
"ensure_directory",
|
|
58
|
+
"fail",
|
|
59
|
+
"load_recipe",
|
|
60
|
+
"output_type",
|
|
61
|
+
"parallel",
|
|
62
|
+
"prepare_context",
|
|
63
|
+
"recipe",
|
|
64
|
+
"record_side_effect",
|
|
65
|
+
"resource",
|
|
66
|
+
"run_command",
|
|
67
|
+
"step",
|
|
68
|
+
"tool",
|
|
69
|
+
"verify_artifact",
|
|
70
|
+
"word_count",
|
|
71
|
+
"write_json",
|
|
72
|
+
"write_text",
|
|
73
|
+
]
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
"""Strict role-to-provider configuration for host agents."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import re
|
|
7
|
+
from collections.abc import Mapping
|
|
8
|
+
from dataclasses import dataclass, field, replace
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any, cast
|
|
11
|
+
|
|
12
|
+
from .exceptions import CalcifierError, ErrorFamily
|
|
13
|
+
from .hashing import sha256_hex
|
|
14
|
+
from .json_config import load_json_config
|
|
15
|
+
from .validation import is_positive_number
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class AgentConfigError(CalcifierError, ValueError):
|
|
19
|
+
"""Raised when an agent configuration is invalid."""
|
|
20
|
+
|
|
21
|
+
family = ErrorFamily.SETUP
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
ROLE_RE = re.compile(r"[A-Za-z][A-Za-z0-9._-]*")
|
|
25
|
+
_MODEL_RE = re.compile(r"[A-Za-z0-9][A-Za-z0-9._:/-]*")
|
|
26
|
+
BUILTIN_AGENTS = frozenset({"claude", "codex"})
|
|
27
|
+
# Claude's whole tool allowlist: agents read and search, and never write or run commands.
|
|
28
|
+
_CLAUDE_READ_TOOLS = "Read,Glob,Grep"
|
|
29
|
+
_CODEX_REASONING_EFFORTS = frozenset(
|
|
30
|
+
{"none", "minimal", "low", "medium", "high", "xhigh", "max", "ultra"}
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass(frozen=True)
|
|
35
|
+
class AgentRuntime:
|
|
36
|
+
kind: str
|
|
37
|
+
|
|
38
|
+
def as_dict(self) -> dict[str, str]:
|
|
39
|
+
return {"kind": self.kind}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass(frozen=True)
|
|
43
|
+
class AgentRoute:
|
|
44
|
+
role: str
|
|
45
|
+
builtin: str | None
|
|
46
|
+
command: tuple[str, ...] | None
|
|
47
|
+
runtime: AgentRuntime
|
|
48
|
+
timeout_seconds: int | None = None
|
|
49
|
+
remediation: str | None = None
|
|
50
|
+
model: str | None = None
|
|
51
|
+
reasoning_effort: str | None = None
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def target_type(self) -> str:
|
|
55
|
+
return "builtin" if self.builtin is not None else "command"
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def target_name(self) -> str:
|
|
59
|
+
if self.builtin is not None:
|
|
60
|
+
return self.builtin
|
|
61
|
+
if self.command is None: # defensive; parser enforces exactly one target
|
|
62
|
+
raise AgentConfigError(f"agent {self.role!r} has no target")
|
|
63
|
+
return self.command[0]
|
|
64
|
+
|
|
65
|
+
def as_dict(self) -> dict[str, Any]:
|
|
66
|
+
target: dict[str, Any]
|
|
67
|
+
if self.builtin is not None:
|
|
68
|
+
target = {"builtin": self.builtin}
|
|
69
|
+
else:
|
|
70
|
+
target = {"command": list(self.command or ())}
|
|
71
|
+
result: dict[str, Any] = {
|
|
72
|
+
"target": target,
|
|
73
|
+
"runtime": self.runtime.as_dict(),
|
|
74
|
+
}
|
|
75
|
+
if self.timeout_seconds is not None:
|
|
76
|
+
result["timeout_seconds"] = self.timeout_seconds
|
|
77
|
+
if self.remediation is not None:
|
|
78
|
+
result["remediation"] = self.remediation
|
|
79
|
+
if self.model is not None:
|
|
80
|
+
result["model"] = self.model
|
|
81
|
+
if self.reasoning_effort is not None:
|
|
82
|
+
result["reasoning_effort"] = self.reasoning_effort
|
|
83
|
+
return result
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@dataclass(frozen=True)
|
|
87
|
+
class AgentConfiguration:
|
|
88
|
+
routes: Mapping[str, AgentRoute] = field(default_factory=dict)
|
|
89
|
+
|
|
90
|
+
def fingerprint(self, roles: tuple[str, ...] | None = None) -> str:
|
|
91
|
+
selected = sorted(self.routes) if roles is None else sorted(set(roles))
|
|
92
|
+
payload = {
|
|
93
|
+
"version": 1,
|
|
94
|
+
"agents": {
|
|
95
|
+
role: self.routes[role].as_dict() for role in selected if role in self.routes
|
|
96
|
+
},
|
|
97
|
+
}
|
|
98
|
+
# gremlin: pardon[equivalent] the payload is already built in sorted, fixed order.
|
|
99
|
+
material = json.dumps(payload, sort_keys=True, separators=(",", ":"))
|
|
100
|
+
return sha256_hex(material.encode())
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def override_agent_timeouts(
|
|
104
|
+
configuration: AgentConfiguration,
|
|
105
|
+
values: list[str] | tuple[str, ...] | None,
|
|
106
|
+
) -> AgentConfiguration:
|
|
107
|
+
"""Apply strict ``ROLE=SECONDS`` overrides to a version-1 configuration."""
|
|
108
|
+
routes = dict(configuration.routes)
|
|
109
|
+
for value in values or ():
|
|
110
|
+
if "=" not in value:
|
|
111
|
+
raise AgentConfigError("--agent-timeout expects ROLE=SECONDS")
|
|
112
|
+
role, raw_seconds = value.split("=", 1)
|
|
113
|
+
if role not in routes:
|
|
114
|
+
raise AgentConfigError(f"--agent-timeout names unknown role {role!r}")
|
|
115
|
+
try:
|
|
116
|
+
seconds = int(raw_seconds)
|
|
117
|
+
except ValueError as exc:
|
|
118
|
+
raise AgentConfigError("--agent-timeout seconds must be an integer") from exc
|
|
119
|
+
if not is_positive_number(seconds, integral=True):
|
|
120
|
+
raise AgentConfigError("--agent-timeout seconds must be positive")
|
|
121
|
+
routes[role] = replace(routes[role], timeout_seconds=seconds)
|
|
122
|
+
return AgentConfiguration(routes=routes)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def load_agent_config(path: str | Path) -> AgentConfiguration:
|
|
126
|
+
"""Load a strict version-1 role-to-subprocess configuration."""
|
|
127
|
+
config_path = Path(path)
|
|
128
|
+
try:
|
|
129
|
+
raw = load_json_config(config_path, object_pairs_hook=_unique_object)
|
|
130
|
+
except (OSError, json.JSONDecodeError) as exc:
|
|
131
|
+
raise AgentConfigError(f"failed to load agent config {config_path}: {exc}") from exc
|
|
132
|
+
root = _mapping(raw, "agent config")
|
|
133
|
+
_reject_unknown(root, {"version", "agents"}, "agent config")
|
|
134
|
+
if root.get("version") != 1:
|
|
135
|
+
raise AgentConfigError("agent config version must be 1")
|
|
136
|
+
raw_agents = _mapping(root.get("agents"), "agent config agents")
|
|
137
|
+
routes: dict[str, AgentRoute] = {}
|
|
138
|
+
for role, value in raw_agents.items():
|
|
139
|
+
if not isinstance(role, str) or ROLE_RE.fullmatch(role) is None:
|
|
140
|
+
raise AgentConfigError(f"invalid agent role name: {role!r}")
|
|
141
|
+
if role in BUILTIN_AGENTS:
|
|
142
|
+
raise AgentConfigError(f"agent role {role!r} is reserved")
|
|
143
|
+
routes[role] = _parse_route(role, value)
|
|
144
|
+
return AgentConfiguration(routes=routes)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _unique_object(pairs: list[tuple[str, Any]]) -> dict[str, Any]:
|
|
148
|
+
result: dict[str, Any] = {}
|
|
149
|
+
for key, value in pairs:
|
|
150
|
+
if key in result:
|
|
151
|
+
raise AgentConfigError(f"agent config contains duplicate key {key!r}")
|
|
152
|
+
result[key] = value
|
|
153
|
+
return result
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _parse_route(role: str, raw: object) -> AgentRoute:
|
|
157
|
+
route = _mapping(raw, f"agent {role!r}")
|
|
158
|
+
_reject_unknown(
|
|
159
|
+
route,
|
|
160
|
+
{
|
|
161
|
+
"target",
|
|
162
|
+
"runtime",
|
|
163
|
+
"timeout_seconds",
|
|
164
|
+
"remediation",
|
|
165
|
+
"model",
|
|
166
|
+
"reasoning_effort",
|
|
167
|
+
},
|
|
168
|
+
f"agent {role!r}",
|
|
169
|
+
)
|
|
170
|
+
target = _mapping(route.get("target"), f"agent {role!r} target")
|
|
171
|
+
_reject_unknown(target, {"builtin", "command"}, f"agent {role!r} target")
|
|
172
|
+
has_builtin = "builtin" in target
|
|
173
|
+
has_command = "command" in target
|
|
174
|
+
if has_builtin == has_command:
|
|
175
|
+
raise AgentConfigError(
|
|
176
|
+
f"agent {role!r} target must contain exactly one of builtin or command"
|
|
177
|
+
)
|
|
178
|
+
builtin: str | None = None
|
|
179
|
+
command: tuple[str, ...] | None = None
|
|
180
|
+
if has_builtin:
|
|
181
|
+
builtin_value = target["builtin"]
|
|
182
|
+
if builtin_value not in BUILTIN_AGENTS:
|
|
183
|
+
raise AgentConfigError(
|
|
184
|
+
f"agent {role!r} builtin must be one of {sorted(BUILTIN_AGENTS)}"
|
|
185
|
+
)
|
|
186
|
+
builtin = str(builtin_value)
|
|
187
|
+
else:
|
|
188
|
+
command_value = target["command"]
|
|
189
|
+
if not isinstance(command_value, list) or not command_value:
|
|
190
|
+
raise AgentConfigError(f"agent {role!r} command must be a non-empty argv array")
|
|
191
|
+
if not all(isinstance(part, str) and part for part in command_value):
|
|
192
|
+
raise AgentConfigError(f"agent {role!r} command entries must be non-empty strings")
|
|
193
|
+
command = tuple(command_value)
|
|
194
|
+
runtime = _parse_runtime(role, route.get("runtime"))
|
|
195
|
+
timeout = _parse_timeout(role, route.get("timeout_seconds"))
|
|
196
|
+
remediation = route.get("remediation")
|
|
197
|
+
if remediation is not None and not isinstance(remediation, str):
|
|
198
|
+
raise AgentConfigError(f"agent {role!r} remediation must be a string")
|
|
199
|
+
model = _parse_model(role, route.get("model"), builtin)
|
|
200
|
+
reasoning_effort = _parse_codex_reasoning_effort(role, route.get("reasoning_effort"), builtin)
|
|
201
|
+
return AgentRoute(
|
|
202
|
+
role,
|
|
203
|
+
builtin,
|
|
204
|
+
command,
|
|
205
|
+
runtime,
|
|
206
|
+
timeout,
|
|
207
|
+
remediation,
|
|
208
|
+
model,
|
|
209
|
+
reasoning_effort,
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def _parse_model(role: str, value: object, builtin: str | None) -> str | None:
|
|
214
|
+
if value is None:
|
|
215
|
+
return None
|
|
216
|
+
if builtin not in {"claude", "codex"}:
|
|
217
|
+
raise AgentConfigError(f"agent {role!r} model is only valid for builtin claude or codex")
|
|
218
|
+
if not isinstance(value, str) or _MODEL_RE.fullmatch(value) is None:
|
|
219
|
+
raise AgentConfigError(f"agent {role!r} model must be a non-empty model name")
|
|
220
|
+
return value
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def _parse_codex_reasoning_effort(role: str, value: object, builtin: str | None) -> str | None:
|
|
224
|
+
if value is None:
|
|
225
|
+
return None
|
|
226
|
+
if builtin != "codex":
|
|
227
|
+
raise AgentConfigError(f"agent {role!r} reasoning_effort is only valid for builtin codex")
|
|
228
|
+
if value not in _CODEX_REASONING_EFFORTS:
|
|
229
|
+
raise AgentConfigError(
|
|
230
|
+
f"agent {role!r} reasoning_effort must be one of {sorted(_CODEX_REASONING_EFFORTS)}"
|
|
231
|
+
)
|
|
232
|
+
return str(value)
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def _parse_timeout(role: str, value: object) -> int:
|
|
236
|
+
if value is None:
|
|
237
|
+
raise AgentConfigError(f"agent {role!r} requires timeout_seconds")
|
|
238
|
+
if not is_positive_number(value, integral=True):
|
|
239
|
+
raise AgentConfigError(f"agent {role!r} timeout_seconds must be a positive integer")
|
|
240
|
+
return cast("int", value)
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def _parse_runtime(role: str, raw: object) -> AgentRuntime:
|
|
244
|
+
runtime = _mapping(raw, f"agent {role!r} runtime")
|
|
245
|
+
if runtime.get("kind") != "host":
|
|
246
|
+
raise AgentConfigError(f"agent {role!r} runtime kind must be host")
|
|
247
|
+
if set(runtime) != {"kind"}:
|
|
248
|
+
raise AgentConfigError(f"host runtime for agent {role!r} accepts only kind")
|
|
249
|
+
return AgentRuntime(kind="host")
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def _mapping(raw: object, context: str) -> Mapping[str, Any]:
|
|
253
|
+
if not isinstance(raw, Mapping):
|
|
254
|
+
raise AgentConfigError(f"{context} must be a JSON object")
|
|
255
|
+
if not all(isinstance(key, str) for key in raw):
|
|
256
|
+
raise AgentConfigError(f"{context} keys must be strings")
|
|
257
|
+
return cast("Mapping[str, Any]", raw)
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def _reject_unknown(raw: Mapping[str, Any], allowed: set[str], context: str) -> None:
|
|
261
|
+
unknown = sorted(set(raw) - allowed)
|
|
262
|
+
if unknown:
|
|
263
|
+
raise AgentConfigError(f"{context} has unknown fields: {unknown}")
|