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.
- sliceagent/__init__.py +3 -0
- sliceagent/__main__.py +6 -0
- sliceagent/access.py +93 -0
- sliceagent/agents.py +173 -0
- sliceagent/background_review.py +146 -0
- sliceagent/binsniff.py +89 -0
- sliceagent/cli.py +890 -0
- sliceagent/clock.py +32 -0
- sliceagent/code_grep.py +329 -0
- sliceagent/code_index.py +417 -0
- sliceagent/config.py +240 -0
- sliceagent/context_overflow.py +227 -0
- sliceagent/envspec.py +129 -0
- sliceagent/errors.py +167 -0
- sliceagent/events.py +96 -0
- sliceagent/finding_types.py +70 -0
- sliceagent/flags.py +63 -0
- sliceagent/fuzzy.py +135 -0
- sliceagent/guardrails.py +438 -0
- sliceagent/guidance.py +69 -0
- sliceagent/hippocampus.py +581 -0
- sliceagent/hooks.py +334 -0
- sliceagent/interfaces.py +144 -0
- sliceagent/llm.py +695 -0
- sliceagent/loop.py +548 -0
- sliceagent/mcp_client.py +255 -0
- sliceagent/mcp_security.py +77 -0
- sliceagent/memory.py +428 -0
- sliceagent/metrics.py +103 -0
- sliceagent/model_catalog.py +124 -0
- sliceagent/monitor.py +615 -0
- sliceagent/neocortex.py +436 -0
- sliceagent/onboarding.py +323 -0
- sliceagent/oracle.py +36 -0
- sliceagent/pagetable.py +255 -0
- sliceagent/pfc.py +449 -0
- sliceagent/plugins.py +127 -0
- sliceagent/policy.py +234 -0
- sliceagent/procman.py +187 -0
- sliceagent/prompt.py +239 -0
- sliceagent/records.py +108 -0
- sliceagent/recovery.py +119 -0
- sliceagent/regions.py +678 -0
- sliceagent/registry.py +128 -0
- sliceagent/retriever.py +19 -0
- sliceagent/safety.py +332 -0
- sliceagent/sandbox.py +143 -0
- sliceagent/scheduler.py +92 -0
- sliceagent/search_index.py +289 -0
- sliceagent/seed.py +465 -0
- sliceagent/sensory_cortex.py +500 -0
- sliceagent/session.py +222 -0
- sliceagent/skill_provenance.py +71 -0
- sliceagent/skill_usage.py +123 -0
- sliceagent/skills.py +209 -0
- sliceagent/subagent.py +332 -0
- sliceagent/subdir_hints.py +222 -0
- sliceagent/swap.py +182 -0
- sliceagent/taskstate.py +57 -0
- sliceagent/telemetry.py +59 -0
- sliceagent/terminal.py +240 -0
- sliceagent/text_utils.py +56 -0
- sliceagent/tool_summary.py +93 -0
- sliceagent/tools.py +1194 -0
- sliceagent/tui.py +1377 -0
- sliceagent/web.py +354 -0
- sliceagent-0.1.0.dist-info/METADATA +262 -0
- sliceagent-0.1.0.dist-info/RECORD +71 -0
- sliceagent-0.1.0.dist-info/WHEEL +4 -0
- sliceagent-0.1.0.dist-info/entry_points.txt +2 -0
- 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
|