arkaos 4.16.0 → 4.18.0

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.
@@ -11,6 +11,77 @@ from datetime import datetime
11
11
  from pathlib import Path
12
12
 
13
13
  from core.agents.loader import load_agent
14
+ from core.agents.provenance import Provenance, agent_provenance
15
+
16
+
17
+ def _provenance_entry(prov: Provenance) -> dict:
18
+ """The full origin/source/licence trail for the registry — not just
19
+ origin, so the shipped file can answer 'what licence is this under?'."""
20
+ entry = {"origin": prov.origin}
21
+ if not prov.is_first_party:
22
+ entry["source"] = prov.source
23
+ entry["license"] = prov.license
24
+ return entry
25
+
26
+
27
+ def _dna_dict(dna) -> dict:
28
+ """The four-framework behavioral DNA, flattened for the registry."""
29
+ return {
30
+ "disc": {
31
+ "primary": dna.disc.primary.value,
32
+ "secondary": dna.disc.secondary.value,
33
+ "label": dna.disc.label,
34
+ },
35
+ "enneagram": {
36
+ "type": dna.enneagram.type.value,
37
+ "wing": dna.enneagram.wing,
38
+ "label": dna.enneagram.label,
39
+ },
40
+ "big_five": {
41
+ "O": dna.big_five.openness,
42
+ "C": dna.big_five.conscientiousness,
43
+ "E": dna.big_five.extraversion,
44
+ "A": dna.big_five.agreeableness,
45
+ "N": dna.big_five.neuroticism,
46
+ },
47
+ "mbti": dna.mbti.type.value,
48
+ }
49
+
50
+
51
+ def _agent_entry(agent, prov: Provenance, yaml_file: Path, root: Path) -> dict:
52
+ """One registry entry for a loaded agent + its provenance."""
53
+ return {
54
+ "id": agent.id,
55
+ "name": agent.name,
56
+ "role": agent.role,
57
+ "department": agent.department,
58
+ "tier": agent.tier,
59
+ "model": agent.get_model(),
60
+ "provenance": _provenance_entry(prov),
61
+ "parent_squad": agent.parent_squad,
62
+ "sub_squad_role": agent.sub_squad_role,
63
+ **_dna_dict(agent.behavioral_dna),
64
+ "authority": {
65
+ k: v for k, v in agent.authority.model_dump().items()
66
+ if v and v != [] and k not in ("delegates_to", "escalates_to")
67
+ },
68
+ "expertise_domains": agent.expertise.domains,
69
+ "frameworks": agent.expertise.frameworks,
70
+ "knowledge_sources": agent.expertise.knowledge_sources,
71
+ "file": str(yaml_file.relative_to(root)),
72
+ "memory_path": agent.memory_path,
73
+ }
74
+
75
+
76
+ def _summaries(agents: list[dict]) -> dict:
77
+ """Tier, department, and DISC-primary counts for the registry meta."""
78
+ tiers, depts, disc = {}, {}, {}
79
+ for a in agents:
80
+ tiers[a["tier"]] = tiers.get(a["tier"], 0) + 1
81
+ depts[a["department"]] = depts.get(a["department"], 0) + 1
82
+ primary = a["disc"]["primary"]
83
+ disc[primary] = disc.get(primary, 0) + 1
84
+ return {"tiers": tiers, "departments": depts, "disc_distribution": disc}
14
85
 
15
86
 
16
87
  def generate_registry(departments_dir: str | Path, output_path: str | Path) -> dict:
@@ -34,72 +105,19 @@ def generate_registry(departments_dir: str | Path, output_path: str | Path) -> d
34
105
  for yaml_file in sorted(departments_dir.glob("*/agents/**/*.yaml")):
35
106
  try:
36
107
  agent = load_agent(yaml_file)
37
- entry = {
38
- "id": agent.id,
39
- "name": agent.name,
40
- "role": agent.role,
41
- "department": agent.department,
42
- "tier": agent.tier,
43
- "model": agent.get_model(),
44
- "parent_squad": agent.parent_squad,
45
- "sub_squad_role": agent.sub_squad_role,
46
- "disc": {
47
- "primary": agent.behavioral_dna.disc.primary.value,
48
- "secondary": agent.behavioral_dna.disc.secondary.value,
49
- "label": agent.behavioral_dna.disc.label,
50
- },
51
- "enneagram": {
52
- "type": agent.behavioral_dna.enneagram.type.value,
53
- "wing": agent.behavioral_dna.enneagram.wing,
54
- "label": agent.behavioral_dna.enneagram.label,
55
- },
56
- "big_five": {
57
- "O": agent.behavioral_dna.big_five.openness,
58
- "C": agent.behavioral_dna.big_five.conscientiousness,
59
- "E": agent.behavioral_dna.big_five.extraversion,
60
- "A": agent.behavioral_dna.big_five.agreeableness,
61
- "N": agent.behavioral_dna.big_five.neuroticism,
62
- },
63
- "mbti": agent.behavioral_dna.mbti.type.value,
64
- "authority": {
65
- k: v for k, v in agent.authority.model_dump().items()
66
- if v and v != [] and k not in ("delegates_to", "escalates_to")
67
- },
68
- "expertise_domains": agent.expertise.domains,
69
- "frameworks": agent.expertise.frameworks,
70
- "knowledge_sources": agent.expertise.knowledge_sources,
71
- "file": str(yaml_file.relative_to(departments_dir.parent)),
72
- "memory_path": agent.memory_path,
73
- }
74
- agents.append(entry)
108
+ prov = agent_provenance(yaml_file)
109
+ agents.append(_agent_entry(
110
+ agent, prov, yaml_file, departments_dir.parent))
75
111
  except Exception as e:
76
112
  errors.append(f"{yaml_file.name}: {e}")
77
113
 
78
- # Build tier summary
79
- tier_counts = {}
80
- for a in agents:
81
- tier_counts[a["tier"]] = tier_counts.get(a["tier"], 0) + 1
82
-
83
- # Build department summary
84
- dept_counts = {}
85
- for a in agents:
86
- dept_counts[a["department"]] = dept_counts.get(a["department"], 0) + 1
87
-
88
- # Build DISC distribution
89
- disc_counts = {}
90
- for a in agents:
91
- p = a["disc"]["primary"]
92
- disc_counts[p] = disc_counts.get(p, 0) + 1
93
-
94
114
  registry = {
95
115
  "_meta": {
96
116
  "version": "2.0.0",
97
117
  "generated": datetime.now().isoformat(),
98
118
  "total_agents": len(agents),
99
119
  "generator": "core/agents/registry_gen.py",
100
- "tiers": tier_counts,
101
- "departments": dept_counts,
102
- "disc_distribution": disc_counts,
120
+ **_summaries(agents),
103
121
  },
104
122
  "agents": agents,
105
123
  }
@@ -5,10 +5,14 @@ project filtering, presentation lifecycle, and dismissal analytics.
5
5
  """
6
6
 
7
7
  import sqlite3
8
- from datetime import datetime, timezone
8
+ from datetime import UTC, datetime
9
9
  from pathlib import Path
10
10
 
11
- from core.cognition.memory.schemas import ActionableInsight
11
+ from core.cognition.memory.schemas import (
12
+ INSTINCT_CONFIDENCE_MAX,
13
+ INSTINCT_CONFIDENCE_MIN,
14
+ ActionableInsight,
15
+ )
12
16
 
13
17
 
14
18
  class InsightStore:
@@ -26,6 +30,27 @@ class InsightStore:
26
30
  conn.execute("PRAGMA journal_mode=WAL")
27
31
  return conn
28
32
 
33
+ # Instinct columns added after the original 12-column schema. An
34
+ # operator's existing DB has the old shape, so these are ALTER-added
35
+ # with defaults rather than baked into CREATE TABLE — a fresh install
36
+ # gets them from CREATE, an upgrade gets them from _migrate.
37
+ #
38
+ # Each entry carries the FULL, literal ALTER statement — never an
39
+ # f-string built from parts. SQLite cannot parameterise a DDL
40
+ # identifier, so the only safe construction is a constant statement,
41
+ # and a constant statement is also the only one a scanner can trust:
42
+ # there is no interpolation site to become an injection the day these
43
+ # names turn dynamic.
44
+ _INSTINCT_COLUMNS = (
45
+ ("confidence",
46
+ "ALTER TABLE insights ADD COLUMN confidence REAL NOT NULL DEFAULT 0.5"),
47
+ ("scope",
48
+ "ALTER TABLE insights ADD COLUMN scope TEXT NOT NULL DEFAULT 'project'"),
49
+ ("evidence_count",
50
+ "ALTER TABLE insights ADD COLUMN evidence_count "
51
+ "INTEGER NOT NULL DEFAULT 1"),
52
+ )
53
+
29
54
  def _init_db(self) -> None:
30
55
  with self._conn() as conn:
31
56
  conn.execute("""
@@ -41,9 +66,13 @@ class InsightStore:
41
66
  recommendation TEXT NOT NULL,
42
67
  context TEXT NOT NULL,
43
68
  status TEXT NOT NULL DEFAULT 'pending',
44
- presented_at TEXT
69
+ presented_at TEXT,
70
+ confidence REAL NOT NULL DEFAULT 0.5,
71
+ scope TEXT NOT NULL DEFAULT 'project',
72
+ evidence_count INTEGER NOT NULL DEFAULT 1
45
73
  )
46
74
  """)
75
+ self._migrate(conn)
47
76
  conn.execute(
48
77
  "CREATE INDEX IF NOT EXISTS idx_insights_project ON insights (project)"
49
78
  )
@@ -51,6 +80,16 @@ class InsightStore:
51
80
  "CREATE INDEX IF NOT EXISTS idx_insights_status ON insights (status)"
52
81
  )
53
82
 
83
+ def _migrate(self, conn: sqlite3.Connection) -> None:
84
+ """Add instinct columns to a pre-existing table. Idempotent."""
85
+ existing = {
86
+ row["name"]
87
+ for row in conn.execute("PRAGMA table_info(insights)").fetchall()
88
+ }
89
+ for name, alter_stmt in self._INSTINCT_COLUMNS:
90
+ if name not in existing:
91
+ conn.execute(alter_stmt)
92
+
54
93
  def _row_to_insight(self, row: sqlite3.Row) -> ActionableInsight:
55
94
  data = dict(row)
56
95
  # Map trigger_source back to trigger (SQL keyword workaround)
@@ -69,8 +108,8 @@ class InsightStore:
69
108
  INSERT OR REPLACE INTO insights
70
109
  (id, project, trigger_source, date_generated, category,
71
110
  severity, title, description, recommendation, context,
72
- status, presented_at)
73
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
111
+ status, presented_at, confidence, scope, evidence_count)
112
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
74
113
  """,
75
114
  (
76
115
  insight.id,
@@ -85,6 +124,9 @@ class InsightStore:
85
124
  insight.context,
86
125
  insight.status,
87
126
  insight.presented_at.isoformat() if insight.presented_at else None,
127
+ insight.confidence,
128
+ insight.scope,
129
+ insight.evidence_count,
88
130
  ),
89
131
  )
90
132
 
@@ -130,11 +172,12 @@ class InsightStore:
130
172
  """Mark insights as presented and record the timestamp."""
131
173
  if not ids:
132
174
  return
133
- now = datetime.now(timezone.utc).isoformat()
175
+ now = datetime.now(UTC).isoformat()
134
176
  placeholders = ",".join("?" * len(ids))
135
177
  with self._conn() as conn:
136
178
  conn.execute(
137
- f"UPDATE insights SET status = 'presented', presented_at = ? WHERE id IN ({placeholders})",
179
+ "UPDATE insights SET status = 'presented', presented_at = ? "
180
+ f"WHERE id IN ({placeholders})",
138
181
  [now, *ids],
139
182
  )
140
183
 
@@ -151,5 +194,69 @@ class InsightStore:
151
194
  ).fetchall()
152
195
  return {r["category"]: r["cnt"] for r in rows}
153
196
 
197
+ def reinforce(self, insight_id: str, step: float = 0.1) -> None:
198
+ """Corroboration: raise confidence (clamped) and count evidence.
199
+
200
+ The clamp lives in ``ActionableInsight``; the SQL bounds mirror it
201
+ so a direct UPDATE cannot push a value past the band either.
202
+ """
203
+ with self._conn() as conn:
204
+ conn.execute(
205
+ """
206
+ UPDATE insights
207
+ SET confidence = MIN(?, confidence + ?),
208
+ evidence_count = evidence_count + 1
209
+ WHERE id = ?
210
+ """,
211
+ (INSTINCT_CONFIDENCE_MAX, step, insight_id),
212
+ )
213
+
214
+ def weaken(self, insight_id: str, step: float = 0.2) -> None:
215
+ """Contradiction: lower confidence (clamped to the band floor).
216
+
217
+ A correction cuts harder than a corroboration lifts — the default
218
+ step is larger — because one contradiction outweighs several
219
+ passive confirmations.
220
+ """
221
+ with self._conn() as conn:
222
+ conn.execute(
223
+ "UPDATE insights SET confidence = MAX(?, confidence - ?) WHERE id = ?",
224
+ (INSTINCT_CONFIDENCE_MIN, step, insight_id),
225
+ )
226
+
227
+ def promotable(
228
+ self, min_projects: int = 2, min_confidence: float = 0.8
229
+ ) -> list[str]:
230
+ """Titles seen as project-scoped across >= min_projects distinct
231
+ projects with mean confidence >= min_confidence.
232
+
233
+ The signal B7 (``/arka promote``) consumes: a pattern that has
234
+ proven itself in more than one project has earned global scope.
235
+ Grouped by title because that is the instinct's stable identity
236
+ across projects (the id is per-occurrence).
237
+ """
238
+ with self._conn() as conn:
239
+ rows = conn.execute(
240
+ """
241
+ SELECT title FROM insights
242
+ WHERE scope = 'project'
243
+ GROUP BY title
244
+ HAVING COUNT(DISTINCT project) >= ?
245
+ AND AVG(confidence) >= ?
246
+ """,
247
+ (min_projects, min_confidence),
248
+ ).fetchall()
249
+ return [r["title"] for r in rows]
250
+
251
+ def promote_to_global(self, title: str) -> int:
252
+ """Flip every project-scoped insight with this title to global.
253
+ Returns the number of rows promoted."""
254
+ with self._conn() as conn:
255
+ cur = conn.execute(
256
+ "UPDATE insights SET scope = 'global' WHERE title = ? AND scope = 'project'",
257
+ (title,),
258
+ )
259
+ return cur.rowcount
260
+
154
261
  def close(self) -> None:
155
262
  """No-op — connections are opened per-operation."""
@@ -7,12 +7,10 @@ Three core models:
7
7
  """
8
8
 
9
9
  import uuid
10
- from datetime import datetime, timezone
11
- from typing import Literal
10
+ from datetime import UTC, datetime
12
11
 
13
12
  from pydantic import BaseModel, Field, field_validator
14
13
 
15
-
16
14
  # --- Allowed categories and statuses ---
17
15
 
18
16
  RAW_CAPTURE_CATEGORIES = ("decision", "solution", "pattern", "error", "config")
@@ -35,7 +33,7 @@ ACTIONABLE_INSIGHT_STATUSES = ("pending", "presented", "accepted", "dismissed")
35
33
 
36
34
 
37
35
  def _utc_now() -> datetime:
38
- return datetime.now(timezone.utc)
36
+ return datetime.now(UTC)
39
37
 
40
38
 
41
39
  def _new_uuid() -> str:
@@ -97,8 +95,26 @@ class KnowledgeEntry(BaseModel):
97
95
  return max(0.0, min(1.0, v))
98
96
 
99
97
 
98
+ # Instinct confidence lives in a band, never at the poles. 0.3 is a weak
99
+ # hypothesis worth keeping; 0.9 is strong but never certainty (an agent
100
+ # that is "certain" from a handful of observations is the failure the
101
+ # band prevents). Mirrors the ECC continuous-learning instinct model.
102
+ INSTINCT_CONFIDENCE_MIN = 0.3
103
+ INSTINCT_CONFIDENCE_MAX = 0.9
104
+ INSTINCT_SCOPES = ("project", "global")
105
+
106
+
100
107
  class ActionableInsight(BaseModel):
101
- """Actionable insight — generated by agents for proactive presentation."""
108
+ """Actionable insight — generated by agents for proactive presentation.
109
+
110
+ Carries the ECC-style instinct fields (confidence, scope,
111
+ evidence_count) so an insight can strengthen on corroboration, weaken
112
+ on contradiction, and be promoted from project to global once the
113
+ same signal is seen across enough projects with high enough mean
114
+ confidence (see ``InsightStore.promotable``). All three default to
115
+ backwards-compatible values, so an insight written before this schema
116
+ reads as a fresh, project-scoped hypothesis.
117
+ """
102
118
 
103
119
  id: str = Field(default_factory=_new_uuid)
104
120
  project: str
@@ -113,6 +129,30 @@ class ActionableInsight(BaseModel):
113
129
  status: str = "pending"
114
130
  presented_at: datetime | None = None
115
131
 
132
+ confidence: float = 0.5
133
+ scope: str = "project"
134
+ evidence_count: int = 1
135
+
136
+ @field_validator("confidence")
137
+ @classmethod
138
+ def clamp_confidence(cls, v: float) -> float:
139
+ """Confidence is bounded to the instinct band, never the poles."""
140
+ return max(INSTINCT_CONFIDENCE_MIN, min(INSTINCT_CONFIDENCE_MAX, v))
141
+
142
+ @field_validator("scope")
143
+ @classmethod
144
+ def validate_scope(cls, v: str) -> str:
145
+ if v not in INSTINCT_SCOPES:
146
+ raise ValueError(
147
+ f"Invalid scope '{v}'. Must be one of: {INSTINCT_SCOPES}"
148
+ )
149
+ return v
150
+
151
+ @field_validator("evidence_count")
152
+ @classmethod
153
+ def evidence_count_is_positive(cls, v: int) -> int:
154
+ return max(1, v)
155
+
116
156
  @field_validator("category")
117
157
  @classmethod
118
158
  def validate_category(cls, v: str) -> str: