sliceagent 0.1.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 (71) hide show
  1. sliceagent/__init__.py +3 -0
  2. sliceagent/__main__.py +6 -0
  3. sliceagent/access.py +93 -0
  4. sliceagent/agents.py +173 -0
  5. sliceagent/background_review.py +146 -0
  6. sliceagent/binsniff.py +89 -0
  7. sliceagent/cli.py +890 -0
  8. sliceagent/clock.py +32 -0
  9. sliceagent/code_grep.py +329 -0
  10. sliceagent/code_index.py +417 -0
  11. sliceagent/config.py +240 -0
  12. sliceagent/context_overflow.py +227 -0
  13. sliceagent/envspec.py +129 -0
  14. sliceagent/errors.py +167 -0
  15. sliceagent/events.py +96 -0
  16. sliceagent/finding_types.py +70 -0
  17. sliceagent/flags.py +63 -0
  18. sliceagent/fuzzy.py +135 -0
  19. sliceagent/guardrails.py +438 -0
  20. sliceagent/guidance.py +69 -0
  21. sliceagent/hippocampus.py +581 -0
  22. sliceagent/hooks.py +334 -0
  23. sliceagent/interfaces.py +144 -0
  24. sliceagent/llm.py +695 -0
  25. sliceagent/loop.py +548 -0
  26. sliceagent/mcp_client.py +255 -0
  27. sliceagent/mcp_security.py +77 -0
  28. sliceagent/memory.py +428 -0
  29. sliceagent/metrics.py +103 -0
  30. sliceagent/model_catalog.py +124 -0
  31. sliceagent/monitor.py +615 -0
  32. sliceagent/neocortex.py +436 -0
  33. sliceagent/onboarding.py +323 -0
  34. sliceagent/oracle.py +36 -0
  35. sliceagent/pagetable.py +255 -0
  36. sliceagent/pfc.py +449 -0
  37. sliceagent/plugins.py +127 -0
  38. sliceagent/policy.py +234 -0
  39. sliceagent/procman.py +187 -0
  40. sliceagent/prompt.py +239 -0
  41. sliceagent/records.py +108 -0
  42. sliceagent/recovery.py +119 -0
  43. sliceagent/regions.py +678 -0
  44. sliceagent/registry.py +128 -0
  45. sliceagent/retriever.py +19 -0
  46. sliceagent/safety.py +332 -0
  47. sliceagent/sandbox.py +143 -0
  48. sliceagent/scheduler.py +92 -0
  49. sliceagent/search_index.py +289 -0
  50. sliceagent/seed.py +465 -0
  51. sliceagent/sensory_cortex.py +500 -0
  52. sliceagent/session.py +222 -0
  53. sliceagent/skill_provenance.py +71 -0
  54. sliceagent/skill_usage.py +123 -0
  55. sliceagent/skills.py +209 -0
  56. sliceagent/subagent.py +332 -0
  57. sliceagent/subdir_hints.py +222 -0
  58. sliceagent/swap.py +182 -0
  59. sliceagent/taskstate.py +57 -0
  60. sliceagent/telemetry.py +59 -0
  61. sliceagent/terminal.py +240 -0
  62. sliceagent/text_utils.py +56 -0
  63. sliceagent/tool_summary.py +93 -0
  64. sliceagent/tools.py +1194 -0
  65. sliceagent/tui.py +1377 -0
  66. sliceagent/web.py +354 -0
  67. sliceagent-0.1.0.dist-info/METADATA +262 -0
  68. sliceagent-0.1.0.dist-info/RECORD +71 -0
  69. sliceagent-0.1.0.dist-info/WHEEL +4 -0
  70. sliceagent-0.1.0.dist-info/entry_points.txt +2 -0
  71. sliceagent-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,70 @@
1
+ """Typed findings schema (item 14a) — deterministic classification of a promoted note into
2
+ one of a small fixed vocabulary, so recall is sharper (a 'decision' reads differently from a
3
+ 'ruled-out' dead end). Pure + deterministic → testable offline, no LLM.
4
+
5
+ Vocabulary (fixed, small — the point is sharp typed recall, not a taxonomy):
6
+ DECISION — a choice was made / an approach was adopted
7
+ RESOLVED — a question got answered / a bug was fixed / it now works
8
+ RULED_OUT — an approach was tried and abandoned / didn't work (a dead end to avoid)
9
+ FILE_TOUCHED — a concrete edit landed (the change set)
10
+ NOTE — fallback: an observation with no stronger signal
11
+
12
+ The classifier reads cheap lexical signals from the note text and the episode meta (was a
13
+ file edited? did an error clear?). It is intentionally conservative: an ambiguous note stays
14
+ NOTE rather than mis-typed. neocortex.py tags promoted lessons with the type; hippocampus.py
15
+ renders it as a leading [TYPE] badge.
16
+
17
+ NO-TRANSCRIPT INVARIANT: classification reads already-stored episode records; it produces a
18
+ tag on a durable note, never new context.
19
+
20
+ PUBLIC SIGNATURES (pinned):
21
+ DECISION, RESOLVED, RULED_OUT, FILE_TOUCHED, NOTE # str constants
22
+ classify_finding(note: str, *, edited: bool = False, had_error: bool = False,
23
+ resolved: bool = False) -> str
24
+ badge(kind: str) -> str # "[decision] " etc. (or "" for NOTE)
25
+ """
26
+ from __future__ import annotations
27
+
28
+ import re
29
+
30
+ DECISION = "decision"
31
+ RESOLVED = "resolved-question"
32
+ RULED_OUT = "ruled-out"
33
+ FILE_TOUCHED = "file-touched"
34
+ NOTE = "note"
35
+
36
+ _RULED_OUT_RE = re.compile(
37
+ r"\b(rule[ds]?\s+out|ruled out|doesn'?t work|didn'?t work|won'?t work|not\s+the\s+"
38
+ r"(cause|issue|problem)|dead\s*end|abandon(ed)?|gave up|turned out not|no longer "
39
+ r"(works|needed)|reverted|backed out)\b", re.I)
40
+ # NOTE: "instead of" is deliberately NOT a ruled-out signal — it reads as a DECISION
41
+ # ("use a queue instead of a list"). Ruled-out needs an explicit negative-outcome phrase.
42
+ _DECISION_RE = re.compile(
43
+ r"\b(decided|decision|chose|choosing|will use|going with|approach|opt(ed)?\s+for|"
44
+ r"settled on|plan to|strategy|prefer)\b", re.I)
45
+ _RESOLVED_RE = re.compile(
46
+ r"\b(fixed|resolved|works now|passing|solved|answer(ed)?|root cause|the (bug|issue|"
47
+ r"problem) was|turned out (to be|that)|because)\b", re.I)
48
+
49
+
50
+ def classify_finding(note: str, *, edited: bool = False, had_error: bool = False,
51
+ resolved: bool = False) -> str:
52
+ """Classify `note` into the fixed vocabulary. Precedence (strongest signal wins):
53
+ RULED_OUT (a dead end is the most valuable-to-flag and easiest to mis-file as RESOLVED) >
54
+ DECISION > RESOLVED > FILE_TOUCHED > NOTE. `edited`/`had_error`/`resolved` come from the
55
+ episode meta and reinforce the structural signal when the text is ambiguous."""
56
+ text = note or ""
57
+ if _RULED_OUT_RE.search(text):
58
+ return RULED_OUT
59
+ if _DECISION_RE.search(text):
60
+ return DECISION
61
+ if _RESOLVED_RE.search(text) or (had_error and resolved):
62
+ return RESOLVED
63
+ if edited:
64
+ return FILE_TOUCHED
65
+ return NOTE
66
+
67
+
68
+ def badge(kind: str) -> str:
69
+ """Leading badge for rendering. NOTE → '' (the unmarked default keeps the index clean)."""
70
+ return "" if (not kind or kind == NOTE) else f"[{kind}] "
sliceagent/flags.py ADDED
@@ -0,0 +1,63 @@
1
+ """Experimental feature flags.
2
+
3
+ A tiny env-driven registry so a not-yet-default feature can ship gated and OFF by default, then be
4
+ flipped on by setting its `default=True` once proven. State is read LIVE from the environment on every
5
+ call (nothing cached) so tests and process-env changes take effect immediately.
6
+
7
+ Precedence:
8
+ 1. master switch AGENT_EXPERIMENTAL_ALL truthy → every flag ON
9
+ 2. per-flag env AGENT_EXPERIMENTAL_<ID> → forces ON/OFF when set to a recognized bool
10
+ 3. the flag's registered `default`
11
+
12
+ Usage: `flags.register(Flag("cron", "Scheduled tasks"))` once at import, then gate with
13
+ `if flags.enabled("cron"): ...`. An unknown id resolves False (typo-safe).
14
+ """
15
+ from __future__ import annotations
16
+
17
+ import os
18
+ from dataclasses import dataclass
19
+
20
+ MASTER_ENV = "AGENT_EXPERIMENTAL_ALL"
21
+
22
+
23
+ @dataclass(frozen=True)
24
+ class Flag:
25
+ id: str
26
+ description: str = ""
27
+ default: bool = False
28
+
29
+ @property
30
+ def env(self) -> str:
31
+ return "AGENT_EXPERIMENTAL_" + self.id.upper()
32
+
33
+
34
+ _FLAGS: dict[str, Flag] = {}
35
+
36
+
37
+ def register(flag: Flag) -> Flag:
38
+ """Register (or replace) a flag. Returns it so a module can `MY = register(Flag(...))`."""
39
+ _FLAGS[flag.id] = flag
40
+ return flag
41
+
42
+
43
+ def _parse_bool(value: str | None) -> bool | None:
44
+ if value is None:
45
+ return None
46
+ v = value.strip().lower()
47
+ if v in ("1", "true", "yes", "on"):
48
+ return True
49
+ if v in ("0", "false", "no", "off", ""):
50
+ return False
51
+ return None # unrecognized → defer to the next precedence level
52
+
53
+
54
+ def enabled(flag_id: str) -> bool:
55
+ f = _FLAGS.get(flag_id)
56
+ if f is None:
57
+ return False # unknown flag → off (typo-safe)
58
+ if _parse_bool(os.environ.get(MASTER_ENV)) is True:
59
+ return True # master switch forces all on
60
+ per = _parse_bool(os.environ.get(f.env))
61
+ if per is not None:
62
+ return per # explicit per-flag override
63
+ return f.default
sliceagent/fuzzy.py ADDED
@@ -0,0 +1,135 @@
1
+ """Indentation-tolerant unique-span finder for str_replace.
2
+
3
+ Implements ONLY the two whitespace-tolerant strategies that are safe enough to
4
+ anchor an edit on (line-trim + indentation-flexible), plus their two position
5
+ helpers. Stripped of all replacement logic: this module NEVER writes. It only
6
+ answers "where, if anywhere uniquely, does ``old`` live in ``content``?" as a
7
+ single ``(start, end)`` character span.
8
+
9
+ The single public entry point is :func:`fuzzy_find_unique`. The caller does the
10
+ actual splice (``content[:start] + new + content[end:]``); keeping find and
11
+ replace separate is what lets the edit tool fall back from an exact match to a
12
+ fuzzy one without this module ever touching file bytes.
13
+
14
+ Uniqueness gate: a strategy that finds 0 or >1 candidates yields nothing. We
15
+ try line-trim first, then indentation-flexible, and return the span only when a
16
+ strategy produces exactly ONE candidate.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ from typing import List, Tuple
22
+
23
+ __all__ = ["fuzzy_find_unique"]
24
+
25
+
26
+ def fuzzy_find_unique(content: str, old: str) -> Tuple[int, int] | None:
27
+ """Return the sole ``(start, end)`` char span matching ``old`` in ``content``.
28
+
29
+ Tries, in order, the line-trimmed strategy then the indentation-flexible
30
+ strategy. Returns the span only when a strategy finds EXACTLY ONE match.
31
+ Returns ``None`` when:
32
+
33
+ - ``old`` is empty,
34
+ - a strategy finds zero candidates and no later strategy matches uniquely,
35
+ - the first strategy that matches finds more than one candidate
36
+ (ambiguous — the caller must supply more context).
37
+
38
+ The returned span is a byte-correct slice into ``content``: for any
39
+ replacement ``new``, ``content[:start] + new + content[end:]`` is the
40
+ edited content. This function never replaces text itself.
41
+ """
42
+ if not old or not old.strip(): # an all-whitespace old has no anchor (matches every blank line → zero-width insert)
43
+ return None
44
+
45
+ for strategy in (_strategy_line_trimmed, _strategy_indentation_flexible):
46
+ matches = strategy(content, old)
47
+ if not matches:
48
+ continue
49
+ if len(matches) > 1:
50
+ # Ambiguous under this strategy. Treat >1 as a hard failure
51
+ # (needs more context) rather than falling through to a looser
52
+ # strategy that would only be more ambiguous.
53
+ return None
54
+ return matches[0]
55
+
56
+ return None
57
+
58
+
59
+ # =============================================================================
60
+ # Matching strategies
61
+ # =============================================================================
62
+
63
+ def _strategy_line_trimmed(content: str, pattern: str) -> List[Tuple[int, int]]:
64
+ """Match line-by-line after stripping leading/trailing whitespace per line."""
65
+ pattern_lines = [line.strip() for line in pattern.split("\n")]
66
+ pattern_normalized = "\n".join(pattern_lines)
67
+
68
+ content_lines = content.split("\n")
69
+ content_normalized_lines = [line.strip() for line in content_lines]
70
+
71
+ return _find_normalized_matches(
72
+ content, content_lines, content_normalized_lines,
73
+ pattern, pattern_normalized,
74
+ )
75
+
76
+
77
+ def _strategy_indentation_flexible(content: str, pattern: str) -> List[Tuple[int, int]]:
78
+ """Match line-by-line ignoring leading indentation entirely.
79
+
80
+ Strips only LEADING whitespace (``lstrip``), so trailing whitespace still
81
+ has to line up — this is intentionally narrower than line-trim and acts as a
82
+ second, indentation-only tolerance pass.
83
+ """
84
+ content_lines = content.split("\n")
85
+ content_stripped_lines = [line.lstrip() for line in content_lines]
86
+ pattern_lines = [line.lstrip() for line in pattern.split("\n")]
87
+
88
+ return _find_normalized_matches(
89
+ content, content_lines, content_stripped_lines,
90
+ pattern, "\n".join(pattern_lines),
91
+ )
92
+
93
+
94
+ # =============================================================================
95
+ # Position helpers
96
+ # =============================================================================
97
+
98
+ def _calculate_line_positions(content_lines: List[str], start_line: int,
99
+ end_line: int, content_length: int) -> Tuple[int, int]:
100
+ """Map a [start_line, end_line) line range to a (start, end) char span.
101
+
102
+ Each line in ``content_lines`` is stored without its trailing newline, so the
103
+ ``+ 1`` per line re-adds the ``\\n`` consumed by ``str.split('\\n')``. The
104
+ end position drops the final newline and is clamped to ``content_length``.
105
+ """
106
+ start_pos = sum(len(line) + 1 for line in content_lines[:start_line])
107
+ end_pos = sum(len(line) + 1 for line in content_lines[:end_line]) - 1
108
+ end_pos = min(content_length, end_pos)
109
+ return start_pos, end_pos
110
+
111
+
112
+ def _find_normalized_matches(content: str, content_lines: List[str],
113
+ content_normalized_lines: List[str],
114
+ pattern: str, pattern_normalized: str) -> List[Tuple[int, int]]:
115
+ """Find every block whose normalized lines equal the normalized pattern.
116
+
117
+ Slides a window of ``len(pattern_norm_lines)`` lines across the normalized
118
+ content; on each equal block it maps the line window back to an
119
+ ORIGINAL-content char span via :func:`_calculate_line_positions`. Returns
120
+ all such spans (the caller enforces uniqueness).
121
+ """
122
+ pattern_norm_lines = pattern_normalized.split("\n")
123
+ num_pattern_lines = len(pattern_norm_lines)
124
+
125
+ matches: List[Tuple[int, int]] = []
126
+
127
+ for i in range(len(content_normalized_lines) - num_pattern_lines + 1):
128
+ block = "\n".join(content_normalized_lines[i:i + num_pattern_lines])
129
+ if block == pattern_normalized:
130
+ start_pos, end_pos = _calculate_line_positions(
131
+ content_lines, i, i + num_pattern_lines, len(content)
132
+ )
133
+ matches.append((start_pos, end_pos))
134
+
135
+ return matches