cairnkit 0.5.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.
cairnkit/__init__.py ADDED
@@ -0,0 +1,143 @@
1
+ """cairnkit: a small agent runtime for business applications.
2
+
3
+ Design and several algorithms are ported from Hermes Agent v0.23.0 (MIT, Nous Research);
4
+ see NOTICE.md. Public API:
5
+ """
6
+
7
+ from cairnkit.agents import AgentConfigError, AgentSpec, load_agent, load_agents, validate_persona
8
+ from cairnkit.compression import (
9
+ LATEST_MESSAGE_MARKER,
10
+ SUMMARY_PREFIX,
11
+ CompressionConfig,
12
+ CompressionResult,
13
+ CompressionState,
14
+ ContextCompressor,
15
+ estimate_tokens,
16
+ )
17
+ from cairnkit.curator import (
18
+ CuratorPolicy,
19
+ CuratorReport,
20
+ SkillActivity,
21
+ Suggestion,
22
+ Transition,
23
+ plan_transitions,
24
+ should_run,
25
+ suggest_consolidation,
26
+ )
27
+ from cairnkit.delegate import make_delegate_tool
28
+ from cairnkit.guardrails import GuardrailConfig
29
+ from cairnkit.llm import (
30
+ ChatModel,
31
+ ClassifiedError,
32
+ FailoverReason,
33
+ FallbackModel,
34
+ ModelAPIError,
35
+ OpenAICompatModel,
36
+ ScriptedModel,
37
+ classify_error,
38
+ openrouter,
39
+ )
40
+ from cairnkit.loop import Agent, RunEvent, RunResult, ToolRecord
41
+ from cairnkit.memory import (
42
+ MemoryItem,
43
+ MemoryStore,
44
+ make_memory_tool,
45
+ render_snapshot,
46
+ screen_memory_write,
47
+ )
48
+ from cairnkit.memory_ops import MemoryChange, MemoryPlan, MemoryPlanError, plan_operations
49
+ from cairnkit.pricing import Price, cost_of, estimate_cost
50
+ from cairnkit.prompt import PromptParts, build_system_prompt
51
+ from cairnkit.review import REVIEW_INSTRUCTION, review_conversation
52
+ from cairnkit.search import SearchBackend, SearchHit, SearchItem, make_search_tool
53
+ from cairnkit.security import first_threat_message, scan_for_threats
54
+ from cairnkit.skills import Skill, SkillError, SkillLibrary, make_skill
55
+ from cairnkit.titles import can_replace, derive_title, generate_title
56
+ from cairnkit.tools import (
57
+ ToolContext,
58
+ ToolError,
59
+ ToolOutcome,
60
+ ToolRegistry,
61
+ ToolSpec,
62
+ tool_error,
63
+ tool_result,
64
+ )
65
+ from cairnkit.types import Message, ModelResponse, ToolCall, Usage
66
+
67
+ __all__ = [
68
+ "Agent",
69
+ "AgentConfigError",
70
+ "AgentSpec",
71
+ "build_system_prompt",
72
+ "can_replace",
73
+ "ChatModel",
74
+ "ClassifiedError",
75
+ "classify_error",
76
+ "CompressionConfig",
77
+ "CompressionResult",
78
+ "CompressionState",
79
+ "ContextCompressor",
80
+ "CuratorPolicy",
81
+ "CuratorReport",
82
+ "SkillActivity",
83
+ "Suggestion",
84
+ "Transition",
85
+ "plan_transitions",
86
+ "should_run",
87
+ "suggest_consolidation",
88
+ "make_skill",
89
+ "validate_persona",
90
+ "LATEST_MESSAGE_MARKER",
91
+ "cost_of",
92
+ "derive_title",
93
+ "estimate_cost",
94
+ "estimate_tokens",
95
+ "FailoverReason",
96
+ "FallbackModel",
97
+ "first_threat_message",
98
+ "generate_title",
99
+ "GuardrailConfig",
100
+ "load_agent",
101
+ "load_agents",
102
+ "make_delegate_tool",
103
+ "make_memory_tool",
104
+ "make_search_tool",
105
+ "MemoryChange",
106
+ "MemoryItem",
107
+ "MemoryPlan",
108
+ "MemoryPlanError",
109
+ "MemoryStore",
110
+ "Message",
111
+ "ModelAPIError",
112
+ "ModelResponse",
113
+ "OpenAICompatModel",
114
+ "openrouter",
115
+ "plan_operations",
116
+ "Price",
117
+ "PromptParts",
118
+ "render_snapshot",
119
+ "review_conversation",
120
+ "REVIEW_INSTRUCTION",
121
+ "RunEvent",
122
+ "RunResult",
123
+ "scan_for_threats",
124
+ "screen_memory_write",
125
+ "ScriptedModel",
126
+ "SearchBackend",
127
+ "SearchHit",
128
+ "SearchItem",
129
+ "Skill",
130
+ "SkillError",
131
+ "SkillLibrary",
132
+ "SUMMARY_PREFIX",
133
+ "tool_error",
134
+ "tool_result",
135
+ "ToolCall",
136
+ "ToolContext",
137
+ "ToolError",
138
+ "ToolOutcome",
139
+ "ToolRecord",
140
+ "ToolRegistry",
141
+ "ToolSpec",
142
+ "Usage",
143
+ ]
cairnkit/agents.py ADDED
@@ -0,0 +1,111 @@
1
+ """Agent definitions loaded from a directory.
2
+
3
+ agents/
4
+ note_structurer/
5
+ agent.toml # name, description, tools, skills, limits
6
+ SOUL.md # persona and rules (stable prompt tier)
7
+
8
+ Loading a directory is all a project needs to declare its agents; code only
9
+ supplies the tools and the model.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import tomllib
15
+ from dataclasses import dataclass, field
16
+ from pathlib import Path
17
+
18
+ from cairnkit.guardrails import GuardrailConfig
19
+ from cairnkit.security import first_threat_message
20
+
21
+
22
+ class AgentConfigError(ValueError):
23
+ pass
24
+
25
+
26
+ @dataclass(frozen=True)
27
+ class AgentSpec:
28
+ name: str
29
+ description: str
30
+ persona: str
31
+ model: str = "default" # key the host maps to a ChatModel
32
+ tools: list[str] = field(default_factory=list)
33
+ skills: list[str] = field(default_factory=list)
34
+ max_iterations: int = 6
35
+ temperature: float | None = None
36
+ max_tokens: int | None = None
37
+ guardrails: GuardrailConfig = field(default_factory=GuardrailConfig)
38
+
39
+
40
+ _ALLOWED_KEYS = {
41
+ "name",
42
+ "description",
43
+ "model",
44
+ "tools",
45
+ "skills",
46
+ "max_iterations",
47
+ "temperature",
48
+ "max_tokens",
49
+ "guardrails",
50
+ }
51
+
52
+
53
+ MAX_PERSONA_CHARS = 12_000
54
+
55
+
56
+ def validate_persona(persona: str) -> str:
57
+ """Rules for a persona edited outside the repository (for example in an admin page)."""
58
+ persona = persona.strip()
59
+ if not persona:
60
+ raise AgentConfigError("the persona is empty")
61
+ if len(persona) > MAX_PERSONA_CHARS:
62
+ raise AgentConfigError(f"the persona may hold at most {MAX_PERSONA_CHARS} characters")
63
+ if threat := first_threat_message(persona, scope="all"):
64
+ raise AgentConfigError(threat)
65
+ return persona
66
+
67
+
68
+ def load_agent(directory: str | Path) -> AgentSpec:
69
+ directory = Path(directory)
70
+ config_path = directory / "agent.toml"
71
+ try:
72
+ data = tomllib.loads(config_path.read_text(encoding="utf-8"))
73
+ except (OSError, tomllib.TOMLDecodeError) as exc:
74
+ raise AgentConfigError(f"{config_path}: {exc}") from exc
75
+ unknown = set(data) - _ALLOWED_KEYS
76
+ if unknown:
77
+ raise AgentConfigError(f"{config_path}: unknown keys {sorted(unknown)}")
78
+ name = data.get("name", directory.name)
79
+ if name != directory.name:
80
+ raise AgentConfigError(f"{config_path}: name {name!r} must match its directory")
81
+ soul = directory / "SOUL.md"
82
+ persona = soul.read_text(encoding="utf-8").strip() if soul.is_file() else ""
83
+ if not persona:
84
+ raise AgentConfigError(f"{directory}: SOUL.md is missing or empty")
85
+ if threat := first_threat_message(persona, scope="all"):
86
+ raise AgentConfigError(f"{soul}: {threat}")
87
+ max_iterations = int(data.get("max_iterations", 6))
88
+ if not 1 <= max_iterations <= 50:
89
+ raise AgentConfigError(f"{config_path}: max_iterations must be 1-50")
90
+ return AgentSpec(
91
+ name=name,
92
+ description=str(data.get("description", "")),
93
+ persona=persona,
94
+ model=str(data.get("model", "default")),
95
+ tools=list(data.get("tools", [])),
96
+ skills=list(data.get("skills", [])),
97
+ max_iterations=max_iterations,
98
+ temperature=data.get("temperature"),
99
+ max_tokens=data.get("max_tokens"),
100
+ guardrails=GuardrailConfig(**data.get("guardrails", {})),
101
+ )
102
+
103
+
104
+ def load_agents(root: str | Path) -> dict[str, AgentSpec]:
105
+ root = Path(root)
106
+ if not root.is_dir():
107
+ raise AgentConfigError(f"agents directory not found: {root}")
108
+ return {
109
+ spec.name: spec
110
+ for spec in (load_agent(d) for d in sorted(root.iterdir()) if (d / "agent.toml").is_file())
111
+ }