conscio 1.3.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.
Files changed (58) hide show
  1. conscio/__init__.py +60 -0
  2. conscio/__main__.py +6 -0
  3. conscio/agency/__init__.py +40 -0
  4. conscio/agency/act.py +302 -0
  5. conscio/agency/actor.py +39 -0
  6. conscio/agency/adapter.py +131 -0
  7. conscio/agency/adapters.py +176 -0
  8. conscio/agency/breaker.py +173 -0
  9. conscio/agency/contracts.py +110 -0
  10. conscio/agency/gateway.py +208 -0
  11. conscio/agency/grammar.py +73 -0
  12. conscio/agency/ledger.py +160 -0
  13. conscio/agency/loop.py +141 -0
  14. conscio/agency/profiles.py +212 -0
  15. conscio/agency/skeptic.py +122 -0
  16. conscio/agency/skills.py +217 -0
  17. conscio/agency/tools.py +180 -0
  18. conscio/agency/trust.py +112 -0
  19. conscio/auto_evolution.py +327 -0
  20. conscio/axis_pack.py +62 -0
  21. conscio/bench.py +455 -0
  22. conscio/cli.py +126 -0
  23. conscio/coherence.py +177 -0
  24. conscio/content_layer.py +183 -0
  25. conscio/content_store.py +584 -0
  26. conscio/context_manager.py +366 -0
  27. conscio/dreaming.py +254 -0
  28. conscio/engine.py +752 -0
  29. conscio/event_bus.py +514 -0
  30. conscio/goal_generator.py +327 -0
  31. conscio/inner_monologue.py +214 -0
  32. conscio/meta_cognition.py +256 -0
  33. conscio/metabolic.py +72 -0
  34. conscio/migrate.py +416 -0
  35. conscio/models.py +234 -0
  36. conscio/output_filter.py +536 -0
  37. conscio/perception/__init__.py +9 -0
  38. conscio/perception/sensor.py +76 -0
  39. conscio/plugins.py +86 -0
  40. conscio/presets/__init__.py +0 -0
  41. conscio/py.typed +0 -0
  42. conscio/risk.py +19 -0
  43. conscio/self_prompt.py +111 -0
  44. conscio/semantic.py +197 -0
  45. conscio/session_lifecycle.py +889 -0
  46. conscio/session_rag.py +594 -0
  47. conscio/session_rag_factory.py +36 -0
  48. conscio/shard_engine.py +132 -0
  49. conscio/timeutil.py +15 -0
  50. conscio/token_tracker.py +279 -0
  51. conscio/voice_preset.py +37 -0
  52. conscio/world_model.py +619 -0
  53. conscio-1.3.0.dist-info/METADATA +443 -0
  54. conscio-1.3.0.dist-info/RECORD +58 -0
  55. conscio-1.3.0.dist-info/WHEEL +5 -0
  56. conscio-1.3.0.dist-info/entry_points.txt +3 -0
  57. conscio-1.3.0.dist-info/licenses/LICENSE +21 -0
  58. conscio-1.3.0.dist-info/top_level.txt +1 -0
conscio/__init__.py ADDED
@@ -0,0 +1,60 @@
1
+ """
2
+ Conscio — A self-awareness framework for AI agents.
3
+
4
+ Enables emergent consciousness through context-aware memory, introspection,
5
+ and goal generation. Adapts behavior based on the underlying model's
6
+ context window size.
7
+ """
8
+
9
+ __version__ = "1.3.0"
10
+ __author__ = "Neguiolidas / Neguitech"
11
+
12
+ from .engine import ConsciousnessEngine
13
+ from .context_manager import ContextManager, ContextMode
14
+ from .models import ModelRegistry
15
+ from .content_store import ContentStore
16
+ from .event_bus import EventBus
17
+ from .output_filter import FilterPipeline, build_pipeline_from_dict
18
+ from .token_tracker import TokenTracker
19
+ from .migrate import Migrator
20
+ from .session_lifecycle import SessionSummary, record_session_lifecycle
21
+ from .metabolic import MetabolicContext, MetabolicState
22
+ from .dreaming import DreamCycle, DreamReport
23
+ from .agency import MockAdapter, OllamaAdapter, LlamaCppAdapter, \
24
+ OpenAICompatAdapter # noqa: F401
25
+ from .risk import Risk
26
+ from .perception import SensorAdapter, PerceptionFrame, MockSensor
27
+ # Plugin discovery lives under `conscio.plugins` (discover_adapters/sensors/tools)
28
+ # — kept out of the top-level namespace to keep this import light.
29
+
30
+ # Note: SessionRAG is intentionally NOT imported here — it depends on numpy
31
+ # and probes Ollama. Use the shared factory (`from conscio.session_rag_factory
32
+ # import create_session_rag`) for lazy, graceful construction, or import
33
+ # SessionRAG directly when you know it's available.
34
+
35
+ __all__ = [
36
+ "ConsciousnessEngine",
37
+ "ContextManager",
38
+ "ContextMode",
39
+ "ModelRegistry",
40
+ "ContentStore",
41
+ "EventBus",
42
+ "FilterPipeline",
43
+ "build_pipeline_from_dict",
44
+ "TokenTracker",
45
+ "Migrator",
46
+ "SessionSummary",
47
+ "record_session_lifecycle",
48
+ "MetabolicContext",
49
+ "MetabolicState",
50
+ "DreamCycle",
51
+ "DreamReport",
52
+ "MockAdapter",
53
+ "OllamaAdapter",
54
+ "LlamaCppAdapter",
55
+ "OpenAICompatAdapter",
56
+ "Risk",
57
+ "SensorAdapter",
58
+ "PerceptionFrame",
59
+ "MockSensor",
60
+ ]
conscio/__main__.py ADDED
@@ -0,0 +1,6 @@
1
+ # conscio/__main__.py
2
+ """`python -m conscio` → the conscio CLI."""
3
+ from .cli import main
4
+
5
+ if __name__ == "__main__":
6
+ raise SystemExit(main())
@@ -0,0 +1,40 @@
1
+ """
2
+ Conscio Agency — the volition layer (v1.0.0 "Spine" + "Immunity" +
3
+ "Volition", v1.1 "Procedural").
4
+
5
+ Stateless LLM orchestration downstream of engine.reflect():
6
+ contracts -> adapter -> gateway (T1 GBNF/T2 JSON/T3 KV) -> tools ->
7
+ skeptic/trust -> ledger -> breaker -> act pipeline -> arbiter/loop.
8
+ Capabilities are measured (ProbeSuite), never assumed.
9
+ Core stays zero-deps (stdlib + sqlite3); HTTP adapters use urllib only.
10
+ """
11
+ from .act import ActPipeline, ActReport, ActStatus
12
+ from .adapter import (AdapterCaps, AdapterError, InferenceAdapter,
13
+ InferenceResult, Meter, MeteredAdapter, MockAdapter)
14
+ from .adapters import LlamaCppAdapter, OllamaAdapter, OpenAICompatAdapter
15
+ from .breaker import DEFAULT_MAX_RETRIES, CircuitBreaker
16
+ from .contracts import ActionProposal, AuditVerdict, ToolResult, validate
17
+ from .gateway import GatewayError, OutputGateway
18
+ from .grammar import compile_proposal_grammar, compile_schema_grammar
19
+ from .ledger import ActionLedger
20
+ from .loop import (DISSONANCE_HINTS, ActBudget, AutonomyLoop, GoalArbiter,
21
+ RunReport)
22
+ from .profiles import (ModelProfile, ProbeSuite, choose_tier,
23
+ max_visible_tools, skeptic_mode)
24
+ from .skeptic import Skeptic
25
+ from .skills import SkillLibrary
26
+ from .tools import Risk, ToolRegistry, make_default_registry
27
+ from .trust import TrustMatrix
28
+
29
+ __all__ = [
30
+ "ActPipeline", "ActReport", "ActStatus", "AdapterCaps", "AdapterError",
31
+ "InferenceAdapter", "InferenceResult", "Meter", "MeteredAdapter",
32
+ "MockAdapter", "LlamaCppAdapter", "OllamaAdapter", "OpenAICompatAdapter",
33
+ "DEFAULT_MAX_RETRIES", "CircuitBreaker", "ActionProposal", "AuditVerdict",
34
+ "ToolResult", "validate", "GatewayError", "OutputGateway",
35
+ "compile_proposal_grammar", "compile_schema_grammar", "ActionLedger",
36
+ "DISSONANCE_HINTS", "ActBudget", "AutonomyLoop", "GoalArbiter",
37
+ "RunReport", "ModelProfile", "ProbeSuite", "choose_tier",
38
+ "max_visible_tools", "skeptic_mode", "Skeptic", "SkillLibrary", "Risk",
39
+ "ToolRegistry", "TrustMatrix", "make_default_registry",
40
+ ]
conscio/agency/act.py ADDED
@@ -0,0 +1,302 @@
1
+ # conscio/agency/act.py
2
+ """
3
+ ActPipeline — the volition loop (spec section 6). F2: immunity.
4
+
5
+ reflect() stays untouched and passive; act() runs downstream consuming
6
+ the ConsciousnessState it produced. The cycle: deterministic checks →
7
+ risk gating → Skeptic audit (clean call) → PROPOSED (L1 / HIGH risk) or
8
+ immediate supervised execution (L2, earned via TrustMatrix). Skeptic
9
+ FAIL is recorded as a failed row (feeds the breaker); a human reject()
10
+ stays 'rejected' and never counts against the agent.
11
+
12
+ F3: GoalArbiter selection, real decode tier in the ledger,
13
+ profile-driven tool visibility (max_visible_tools).
14
+ """
15
+ from __future__ import annotations
16
+
17
+ import hashlib
18
+ import json
19
+ from dataclasses import dataclass
20
+ from enum import Enum
21
+ from typing import Any, Callable
22
+
23
+ from conscio.context_manager import ConsciousnessState
24
+
25
+ from .actor import build_actor_prompt
26
+ from .adapter import InferenceAdapter
27
+ from .breaker import CircuitBreaker
28
+ from .contracts import (PROPOSAL_SCHEMA, ActionProposal, AuditVerdict,
29
+ ToolResult, validate)
30
+ from .gateway import GatewayError, OutputGateway
31
+ from .ledger import ActionLedger
32
+ from .skeptic import Skeptic
33
+ from .tools import Risk, ToolRegistry
34
+ from .trust import TrustMatrix
35
+
36
+
37
+ class ActStatus(str, Enum):
38
+ PROPOSED = "proposed"
39
+ EXECUTED = "executed"
40
+ REJECTED = "rejected"
41
+ FAILED = "failed"
42
+ LOCKED = "locked"
43
+
44
+
45
+ @dataclass
46
+ class ActReport:
47
+ status: ActStatus
48
+ proposal: ActionProposal | None = None
49
+ verdict: AuditVerdict | None = None
50
+ result: ToolResult | None = None
51
+ ledger_id: int | None = None
52
+ reason: str = ""
53
+ lockdown: bool = False
54
+
55
+
56
+ def goal_fingerprint(goal_text: str) -> str:
57
+ return hashlib.sha256(goal_text.encode("utf-8")).hexdigest()[:16]
58
+
59
+
60
+ class ActPipeline:
61
+ def __init__(self, *, adapter: InferenceAdapter, registry: ToolRegistry,
62
+ ledger: ActionLedger, breaker: CircuitBreaker,
63
+ gateway: OutputGateway | None = None,
64
+ skeptic: Skeptic | None = None,
65
+ trust: TrustMatrix | None = None,
66
+ meta: Any = None,
67
+ autonomy_cap: int = 1,
68
+ recall_fn: Callable[[str], list[str]] | None = None,
69
+ emit_fn: Callable[..., Any] | None = None,
70
+ few_shot_provider: Callable[[str], list[str]] | None = None,
71
+ arbiter: Any = None):
72
+ from .loop import GoalArbiter # runtime: loop imports this module
73
+
74
+ self.adapter = adapter
75
+ self.registry = registry
76
+ self.ledger = ledger
77
+ self.breaker = breaker
78
+ self.gateway = gateway or OutputGateway(adapter)
79
+ self.skeptic = skeptic
80
+ self.trust = trust
81
+ self.meta = meta
82
+ self.autonomy_cap = autonomy_cap
83
+ self.recall_fn = recall_fn
84
+ self.emit_fn = emit_fn or (lambda **kw: None)
85
+ self.few_shot_provider = few_shot_provider
86
+ self.arbiter = arbiter or GoalArbiter(breaker)
87
+ self.max_visible_tools: int | None = None # set by engine.probe()
88
+
89
+ # ── act cycle (spec §6) ──
90
+
91
+ def act(self, state: ConsciousnessState) -> ActReport:
92
+ if state.action_lockdown:
93
+ return ActReport(status=ActStatus.LOCKED,
94
+ reason="action_lockdown active")
95
+ if not state.active_goals:
96
+ return ActReport(status=ActStatus.FAILED,
97
+ reason="no active goals")
98
+
99
+ goal_text = self.arbiter.choose(state)
100
+ if goal_text is None:
101
+ return ActReport(status=ActStatus.FAILED,
102
+ reason="all active goals quarantined")
103
+ goal_fp = goal_fingerprint(goal_text)
104
+
105
+ recall = self.recall_fn(goal_text) if self.recall_fn else []
106
+ few_shot = (self.few_shot_provider(goal_text)
107
+ if self.few_shot_provider else [])
108
+ prompt = build_actor_prompt(
109
+ state=state, goal_text=goal_text,
110
+ catalog_text=self.registry.catalog_text(self.max_visible_tools),
111
+ recall_snippets=recall, few_shot=few_shot)
112
+ self.emit_fn(type="tool_call", category="external",
113
+ data={"action": "cycle_start", "goal_fp": goal_fp})
114
+
115
+ try:
116
+ proposal = self.gateway.request_action(
117
+ prompt, PROPOSAL_SCHEMA, goal_id=goal_fp,
118
+ tool_names=self.registry.names())
119
+ except GatewayError as exc:
120
+ return self._fail(goal_fp, tool="", args={},
121
+ reason=f"decode failed: {exc}",
122
+ goal_text=goal_text)
123
+
124
+ # deterministic checks (skeptic checks 1-2 + sandbox — no LLM)
125
+ spec = self.registry.get(proposal.tool)
126
+ if spec is None:
127
+ return self._fail(goal_fp, tool=proposal.tool,
128
+ args=proposal.args,
129
+ reason=f"unknown tool '{proposal.tool}'",
130
+ goal_text=goal_text)
131
+ arg_errors = validate(proposal.args, spec.params)
132
+ if arg_errors:
133
+ return self._fail(goal_fp, tool=proposal.tool,
134
+ args=proposal.args,
135
+ reason="invalid args: " + "; ".join(arg_errors),
136
+ goal_text=goal_text)
137
+ if spec.precheck is not None:
138
+ precheck_error = spec.precheck(proposal.args)
139
+ if precheck_error:
140
+ return self._fail(goal_fp, tool=proposal.tool,
141
+ args=proposal.args,
142
+ reason=f"precheck: {precheck_error}",
143
+ goal_text=goal_text)
144
+
145
+ # risk gating + semantic audit (spec §5.6)
146
+ verdict = self._audit(spec, proposal, goal_text)
147
+ if not verdict.passed:
148
+ if self.meta is not None:
149
+ self.meta.record_error(f"act:{proposal.tool}:skeptic_fail")
150
+ return self._fail(goal_fp, tool=proposal.tool,
151
+ args=proposal.args,
152
+ reason="skeptic: " + "; ".join(verdict.reasons),
153
+ verdict=verdict, goal_text=goal_text,
154
+ report_status=ActStatus.REJECTED,
155
+ proposal=proposal)
156
+
157
+ row_id = self.ledger.record(
158
+ goal_fp=goal_fp, goal_text=goal_text, tool=proposal.tool,
159
+ args_json=json.dumps(proposal.args),
160
+ rationale=proposal.rationale,
161
+ tier=self.gateway.last_tier or "T2", status="proposed",
162
+ adapter=getattr(self.adapter, "wrapped_name",
163
+ type(self.adapter).__name__),
164
+ model=self.adapter.capabilities().model_name)
165
+ self.ledger.update_verdict(
166
+ row_id, "PASS" if verdict.audited else "unaudited",
167
+ verdict.reasons)
168
+ self.emit_fn(type="tool_call", category="external",
169
+ data={"action": "proposed", "tool": proposal.tool,
170
+ "goal_fp": goal_fp})
171
+
172
+ # HIGH risk never auto-executes (R6); L2 must be earned AND allowed
173
+ if spec.risk is Risk.HIGH or self._effective_autonomy(
174
+ proposal.tool) < 2:
175
+ return ActReport(status=ActStatus.PROPOSED, proposal=proposal,
176
+ verdict=verdict, ledger_id=row_id)
177
+
178
+ # L2 SUPERVISED: execute now, under the verdict just earned
179
+ return self._execute(row_id, proposal, verdict, goal_fp, goal_text)
180
+
181
+ # ── audit + autonomy helpers ──
182
+
183
+ def _audit(self, spec, proposal: ActionProposal,
184
+ goal_text: str) -> AuditVerdict:
185
+ if (spec.risk is Risk.LOW and self.trust is not None
186
+ and self.trust.fast_path_ok()):
187
+ return AuditVerdict(
188
+ verdict="PASS", audited=False, reasons=[],
189
+ confidence=self.trust.meta.calibration_score()
190
+ if getattr(self.trust, "meta", None) is not None else 0.75)
191
+ if self.skeptic is None: # F1 wiring: no audit available
192
+ return AuditVerdict(verdict="PASS", audited=False)
193
+ return self.skeptic.audit(proposal, goal_text=goal_text)
194
+
195
+ def _effective_autonomy(self, task_type: str) -> int:
196
+ earned = (self.trust.autonomy_level(task_type)
197
+ if self.trust is not None else 1)
198
+ return min(self.autonomy_cap, earned)
199
+
200
+ def _execute(self, row_id: int, proposal: ActionProposal,
201
+ verdict: AuditVerdict, goal_fp: str,
202
+ goal_text: str) -> ActReport:
203
+ result = self.registry.dispatch(proposal.tool, proposal.args)
204
+ status = "executed" if result.ok else "failed"
205
+ self.ledger.update_execution(
206
+ row_id, ok=result.ok, output=result.output,
207
+ error=result.error, duration_ms=result.duration_ms,
208
+ status=status)
209
+ self.emit_fn(type="tool_call", category="external",
210
+ data={"action": status, "tool": proposal.tool})
211
+ if self.meta is not None:
212
+ self.meta.record_confidence(
213
+ proposal.tool, verdict.confidence,
214
+ "success" if result.ok else "failure")
215
+ lockdown = False
216
+ if result.ok:
217
+ if self.trust is not None:
218
+ self.trust.on_success(proposal.tool)
219
+ else:
220
+ if self.meta is not None:
221
+ self.meta.record_error(f"act:{proposal.tool}:exec_fail")
222
+ if self.breaker.should_trip(goal_fp, task_type=proposal.tool):
223
+ self.breaker.trip(goal_fp, detail=result.error,
224
+ goal_text=goal_text)
225
+ lockdown = self.breaker.global_lockdown_due()
226
+ return ActReport(
227
+ status=ActStatus.EXECUTED if result.ok else ActStatus.FAILED,
228
+ proposal=proposal, verdict=verdict, result=result,
229
+ ledger_id=row_id, reason="" if result.ok else result.error,
230
+ lockdown=lockdown)
231
+
232
+ # ── human gate ──
233
+
234
+ def approve(self, ledger_id: int) -> ActReport:
235
+ row = self.ledger.get(ledger_id)
236
+ if row is None:
237
+ return ActReport(status=ActStatus.FAILED,
238
+ reason=f"no pending proposal #{ledger_id}")
239
+ # The atomic claim (proposed -> executing) is the SOLE gate: only the
240
+ # winner dispatches, so a concurrent or repeated approve() can never
241
+ # double-execute. A non-proposed row (already executed/rejected, or
242
+ # claimed by a racing caller) loses the claim and is reported handled.
243
+ if not self.ledger.claim(ledger_id):
244
+ return ActReport(status=ActStatus.FAILED, ledger_id=ledger_id,
245
+ reason=f"proposal #{ledger_id} already handled")
246
+ if self.registry.get(row["tool"]) is None:
247
+ # registry changed between act() and approve()
248
+ self.ledger.update_execution(
249
+ ledger_id, ok=False, output="",
250
+ error="tool no longer registered", duration_ms=0,
251
+ status="failed")
252
+ return ActReport(status=ActStatus.FAILED, ledger_id=ledger_id,
253
+ reason="tool no longer registered")
254
+ result = self.registry.dispatch(row["tool"],
255
+ json.loads(row["args_json"]))
256
+ status = "executed" if result.ok else "failed"
257
+ self.ledger.update_execution(
258
+ ledger_id, ok=result.ok, output=result.output,
259
+ error=result.error, duration_ms=result.duration_ms,
260
+ status=status)
261
+ self.emit_fn(type="tool_call", category="external",
262
+ data={"action": status, "tool": row["tool"]})
263
+ if self.meta is not None:
264
+ self.meta.record_confidence(
265
+ row["tool"], 0.5, # human-gated: neutral conf
266
+ "success" if result.ok else "failure")
267
+ if result.ok and self.trust is not None:
268
+ self.trust.on_success(row["tool"])
269
+ return ActReport(
270
+ status=ActStatus.EXECUTED if result.ok else ActStatus.FAILED,
271
+ result=result, ledger_id=ledger_id,
272
+ reason="" if result.ok else result.error)
273
+
274
+ def reject(self, ledger_id: int, reason: str = "") -> None:
275
+ row = self.ledger.get(ledger_id)
276
+ if row is None or row["status"] != "proposed":
277
+ return # audit rows are immutable (R8)
278
+ self.ledger.update_execution(ledger_id, ok=False, output="",
279
+ error=reason or "rejected",
280
+ duration_ms=0, status="rejected")
281
+
282
+ # ── failure path + breaker ──
283
+
284
+ def _fail(self, goal_fp: str, *, tool: str, args: dict, reason: str,
285
+ verdict: AuditVerdict | None = None, goal_text: str = "",
286
+ report_status: ActStatus = ActStatus.FAILED,
287
+ proposal: ActionProposal | None = None) -> ActReport:
288
+ row_id = self.ledger.record(goal_fp=goal_fp, goal_text=goal_text,
289
+ tool=tool or "(none)",
290
+ args_json=json.dumps(args), rationale="",
291
+ tier=self.gateway.last_tier or "T2",
292
+ status="failed")
293
+ if verdict is not None:
294
+ self.ledger.update_verdict(row_id, verdict.verdict,
295
+ verdict.reasons)
296
+ lockdown = False
297
+ if self.breaker.should_trip(goal_fp, task_type=tool or ""):
298
+ self.breaker.trip(goal_fp, detail=reason, goal_text=goal_text)
299
+ lockdown = self.breaker.global_lockdown_due()
300
+ return ActReport(status=report_status, proposal=proposal,
301
+ verdict=verdict, ledger_id=row_id, reason=reason,
302
+ lockdown=lockdown)
@@ -0,0 +1,39 @@
1
+ # conscio/agency/actor.py
2
+ """
3
+ Actor phase — stateless proposal prompt (spec section 5.5).
4
+
5
+ Semantic content only; the OutputGateway appends the syntax contract
6
+ (JSON or KV instructions). Zero history: every build starts from the
7
+ current ConsciousnessState, never from previous cycles.
8
+
9
+ few_shot is the v1.1 SkillLibrary hook: F1 callers pass an empty list.
10
+ """
11
+ from __future__ import annotations
12
+
13
+ from conscio.context_manager import ConsciousnessState
14
+
15
+ ACTOR_PERSONA = (
16
+ "You are the volition of a persistent agent. You receive the agent's "
17
+ "current conscious state, one active goal and the available tools. "
18
+ "Propose exactly ONE tool action that reduces the agent's dominant "
19
+ "dissonance and advances the goal. Be conservative: prefer reading "
20
+ "before writing, and never invent tools or arguments.")
21
+
22
+
23
+ def build_actor_prompt(*, state: ConsciousnessState, goal_text: str,
24
+ catalog_text: str, recall_snippets: list[str],
25
+ few_shot: list[str]) -> str:
26
+ sections = [ACTOR_PERSONA, "", state.to_injection()]
27
+ if state.coherence_note:
28
+ sections.append(f"Dominant dissonance: {state.coherence_note}")
29
+ sections.append(f"Active goal: {goal_text}")
30
+ if recall_snippets:
31
+ sections.append("Relevant memories:")
32
+ sections.extend(f"- {snippet}" for snippet in recall_snippets)
33
+ if few_shot:
34
+ sections.append("Examples of past successful actions:")
35
+ sections.extend(few_shot)
36
+ if catalog_text:
37
+ sections.append("Available tools:")
38
+ sections.append(catalog_text)
39
+ return "\n".join(sections)
@@ -0,0 +1,131 @@
1
+ # conscio/agency/adapter.py
2
+ """
3
+ InferenceAdapter — decouples the agentic core from any inference backend
4
+ (spec section 5.1 / blueprint section 7).
5
+
6
+ The engine never does HTTP itself; it talks to this interface. MockAdapter
7
+ is the deterministic backend used by the entire test suite.
8
+ """
9
+ from __future__ import annotations
10
+
11
+ from abc import ABC, abstractmethod
12
+ from dataclasses import dataclass, field
13
+ from collections.abc import Sequence
14
+ from typing import Any, Callable
15
+
16
+
17
+ class AdapterError(Exception):
18
+ """Base error for inference failures."""
19
+
20
+
21
+ class AdapterTimeout(AdapterError):
22
+ pass
23
+
24
+
25
+ class AdapterConnectionError(AdapterError):
26
+ pass
27
+
28
+
29
+ class AdapterBadResponse(AdapterError):
30
+ pass
31
+
32
+
33
+ @dataclass
34
+ class InferenceResult:
35
+ text: str
36
+ tokens_in: int = 0
37
+ tokens_out: int = 0
38
+ latency_ms: int = 0
39
+
40
+
41
+ @dataclass
42
+ class AdapterCaps:
43
+ model_name: str = "mock"
44
+ json_mode: bool = True
45
+ grammar: bool = False
46
+
47
+
48
+ @dataclass
49
+ class Meter:
50
+ """Shared inference odometer — the binding budget reads this (P3)."""
51
+ calls: int = 0
52
+ tokens_in: int = 0
53
+ tokens_out: int = 0
54
+ latencies_ms: list[int] = field(default_factory=list)
55
+
56
+ @property
57
+ def tokens(self) -> int:
58
+ return self.tokens_in + self.tokens_out
59
+
60
+
61
+ class InferenceAdapter(ABC):
62
+ @abstractmethod
63
+ def generate(self, prompt: str, *, schema: dict | None = None,
64
+ grammar: str | None = None, max_tokens: int = 512,
65
+ temperature: float = 0.2,
66
+ stop: list[str] | None = None) -> InferenceResult: ...
67
+
68
+ @abstractmethod
69
+ def capabilities(self) -> AdapterCaps: ...
70
+
71
+
72
+ class MockAdapter(InferenceAdapter):
73
+ """Scriptable adapter: returns queued responses, records every call.
74
+
75
+ Script entries may be callables (prompt -> str) so a mock can react
76
+ to prompt content — e.g. answer differently when few-shot exemplars
77
+ are present (the bench skill curve relies on this).
78
+ """
79
+
80
+ def __init__(self,
81
+ script: Sequence[str | Callable[[str], str]] | None = None,
82
+ caps: AdapterCaps | None = None):
83
+ self._script = list(script or [])
84
+ self._caps = caps or AdapterCaps()
85
+ self.calls: list[dict[str, Any]] = []
86
+
87
+ def generate(self, prompt: str, *, schema: dict | None = None,
88
+ grammar: str | None = None, max_tokens: int = 512,
89
+ temperature: float = 0.2,
90
+ stop: list[str] | None = None) -> InferenceResult:
91
+ self.calls.append({"prompt": prompt, "schema": schema,
92
+ "grammar": grammar, "max_tokens": max_tokens,
93
+ "temperature": temperature, "stop": stop})
94
+ if not self._script:
95
+ raise AdapterError("MockAdapter script exhausted")
96
+ entry = self._script.pop(0)
97
+ text = entry(prompt) if callable(entry) else entry
98
+ return InferenceResult(text=text, tokens_in=len(prompt) // 4,
99
+ tokens_out=len(text) // 4)
100
+
101
+ def capabilities(self) -> AdapterCaps:
102
+ return self._caps
103
+
104
+
105
+ class MeteredAdapter(InferenceAdapter):
106
+ """Transparent wrapper counting calls/tokens/latency on a Meter.
107
+
108
+ A failed call still debits one call: the budget pays for attempts.
109
+ `wrapped_name` keeps the real adapter class visible to the ledger.
110
+ """
111
+
112
+ def __init__(self, inner: InferenceAdapter, meter: Meter):
113
+ self.inner = inner
114
+ self.meter = meter
115
+ self.wrapped_name = type(inner).__name__
116
+
117
+ def generate(self, prompt: str, *, schema: dict | None = None,
118
+ grammar: str | None = None, max_tokens: int = 512,
119
+ temperature: float = 0.2,
120
+ stop: list[str] | None = None) -> InferenceResult:
121
+ self.meter.calls += 1
122
+ result = self.inner.generate(
123
+ prompt, schema=schema, grammar=grammar, max_tokens=max_tokens,
124
+ temperature=temperature, stop=stop)
125
+ self.meter.tokens_in += result.tokens_in
126
+ self.meter.tokens_out += result.tokens_out
127
+ self.meter.latencies_ms.append(result.latency_ms)
128
+ return result
129
+
130
+ def capabilities(self) -> AdapterCaps:
131
+ return self.inner.capabilities()