Add sssf skill, installable via the skills CLI
Port the sssf skill from ~/.agents/skills/sssf into this repo so it can be distributed and installed with the skills CLI (skills add INDigitalStudio/skills --skill sssf). - Copy the skill (SKILL.md, cookbooks, references, scripts, templates, and the visualizer app source) into sssf/. - Gitignore build/runtime artifacts: the visualizer's node_modules/ and dist/, Python bytecode, and the machine-specific repos.json. - Make the skill location-independent: install.py now stamps the skill's real path into the stamped justfile's skill_dir (replacing the hardcoded ~/.agents/skills/sssf), so 'just obs' finds the visualizer wherever the CLI installed the skill. - Update cookbooks to use <skill>/scripts/... instead of the hardcoded path, and document the skills CLI install command. - Update the repo README with install instructions.
This commit is contained in:
parent
a42608f602
commit
2cc766aabe
98 changed files with 12508 additions and 0 deletions
210
sssf/templates/adws/adw_modules/agent_omp.py
Normal file
210
sssf/templates/adws/adw_modules/agent_omp.py
Normal file
|
|
@ -0,0 +1,210 @@
|
|||
"""OMP coding agent interface — an alternative to Pi for SSSF.
|
||||
|
||||
Runs `omp -p --mode json` and tails its JSONL stdout line by line, forwarding
|
||||
each event to a callback WHILE the agent works. OMP's event stream is the same
|
||||
shape as Pi's (session / message_start / message_end / turn_end /
|
||||
tool_execution_start / tool_execution_end / agent_end), so the shared
|
||||
ToolCallTracker folds tool calls identically.
|
||||
|
||||
The one real difference is session identity. Pi accepts `--session-id` and
|
||||
creates-or-continues that exact id. OMP mints its own session id and resumes by
|
||||
`--resume <id-prefix>`. To keep SSSF's retry/correction loop (which re-enters
|
||||
the SAME context window) working, this adapter persists the real OMP session id
|
||||
in a `session_id.txt` next to the session dir: the first send creates a session
|
||||
and records its id; every later send with the same session_dir resumes it.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
from functools import lru_cache
|
||||
from pathlib import Path
|
||||
from typing import Callable, Optional
|
||||
|
||||
from .agent_pi import (ToolCallTracker, _context_tokens, _label, _text_of)
|
||||
from .data_types import PiRequest, PiResult
|
||||
from .utils import now_iso, operator_env
|
||||
|
||||
OMP_PATH = os.environ.get("OMP_PATH", "omp")
|
||||
|
||||
RESULT_SNIPPET_CHARS = 20_000 # tool output rides along whole; clip only guards pathological cases
|
||||
ARG_VALUE_CHARS = 20_000 # args too — the UI scrolls, it must not be handed cut-off data
|
||||
LABEL_CHARS = 80 # "bash: <command>" shown as the event name
|
||||
|
||||
# The arg that identifies a call at a glance, in the order tools tend to use.
|
||||
PRIMARY_ARGS = ("command", "path", "file_path", "pattern", "query", "url")
|
||||
|
||||
# The config's `tools` list is written in pi's vocabulary. OMP's tool set
|
||||
# overlaps but is not identical, so translate the names that differ and drop
|
||||
# the ones OMP has no equivalent for (the `writes` boundary, enforced in code,
|
||||
# is what actually keeps an agent read-only — the tool list is a hint).
|
||||
TOOL_TRANSLATION = {
|
||||
"find": "glob", # pi's find -> omp's glob
|
||||
"ls": None, # omp reads directories via `read`
|
||||
"subagent_create": "task", # omp's subagent tool
|
||||
"subagent_continue": "task",
|
||||
"subagent_list": "task",
|
||||
"subagent_remove": "task",
|
||||
}
|
||||
|
||||
|
||||
def _translate_tools(tools: list[str]) -> list[str]:
|
||||
"""Map pi tool names to omp's vocabulary, dropping unmappable ones."""
|
||||
out = []
|
||||
for tool in tools:
|
||||
mapped = TOOL_TRANSLATION.get(tool, tool)
|
||||
if mapped and mapped not in out:
|
||||
out.append(mapped)
|
||||
return out
|
||||
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
def _omp_catalog() -> list[dict]:
|
||||
"""Read omp's merged model catalog (`omp models --json`)."""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
[OMP_PATH, "models", "--json"], capture_output=True, text=True,
|
||||
timeout=30, env=operator_env(), check=False,
|
||||
)
|
||||
except (OSError, subprocess.TimeoutExpired):
|
||||
return []
|
||||
if result.returncode != 0:
|
||||
return []
|
||||
try:
|
||||
return json.loads(result.stdout).get("models", [])
|
||||
except json.JSONDecodeError:
|
||||
return []
|
||||
|
||||
|
||||
def resolve_model(pattern: str) -> tuple[str, str]:
|
||||
"""Resolve a model pattern to an explicit ``(provider, model_id)`` pair.
|
||||
|
||||
OMP's catalog exposes a `selector` of the form ``provider/model-id``. A
|
||||
pattern with a slash must match a selector exactly; a bare pattern matches
|
||||
by model id (substring, then exact), failing on ambiguity.
|
||||
"""
|
||||
catalog = _omp_catalog()
|
||||
selectors = {m.get("selector"): (m.get("provider", ""), m.get("id", ""))
|
||||
for m in catalog if m.get("selector")}
|
||||
if "/" in pattern:
|
||||
if pattern in selectors:
|
||||
return selectors[pattern]
|
||||
raise ValueError(f"model pattern {pattern!r} not found in omp models — "
|
||||
"run `omp models` to see available selectors")
|
||||
matches = [(m.get("provider", ""), m.get("id", "")) for m in catalog
|
||||
if pattern == m.get("id") or pattern in m.get("id", "")]
|
||||
exact = [match for match in matches if match[1] == pattern]
|
||||
if len(exact) == 1:
|
||||
return exact[0]
|
||||
if len(matches) == 1:
|
||||
return matches[0]
|
||||
if not matches:
|
||||
raise ValueError(f"model pattern {pattern!r} not found in omp models — "
|
||||
"run `omp models` to see available models")
|
||||
raise ValueError(f"model pattern {pattern!r} is ambiguous: {matches}")
|
||||
|
||||
|
||||
def context_window(provider: str, model_id: str) -> int:
|
||||
"""The model's context ceiling from omp's merged model catalog."""
|
||||
for model in _omp_catalog():
|
||||
if model.get("provider") == provider and model.get("id") == model_id:
|
||||
return int(model.get("contextWindow") or 0)
|
||||
return 0
|
||||
|
||||
|
||||
def _session_id_file(session_dir: Path) -> Path:
|
||||
return session_dir / "session_id.txt"
|
||||
|
||||
|
||||
def run(request: PiRequest, on_event: Optional[Callable[[dict], None]] = None,
|
||||
on_spawn: Optional[Callable[[int], None]] = None,
|
||||
on_exit: Optional[Callable[[int], None]] = None) -> PiResult:
|
||||
"""Run one non-interactive omp turn.
|
||||
|
||||
`on_spawn(pid)` and `on_exit(pid)` bracket the child process so the caller
|
||||
can record it as killable — a hung coding agent is otherwise a pid you have
|
||||
to hunt for in `ps` while the run sits there.
|
||||
"""
|
||||
provider, model_id = resolve_model(request.model)
|
||||
selector = f"{provider}/{model_id}"
|
||||
|
||||
session_dir = Path(request.session_dir)
|
||||
session_dir.mkdir(parents=True, exist_ok=True)
|
||||
sid_file = _session_id_file(session_dir)
|
||||
|
||||
cmd = [
|
||||
OMP_PATH, "-p", "--mode", "json",
|
||||
"--model", selector,
|
||||
"--thinking", request.thinking,
|
||||
"--system-prompt", request.system_prompt,
|
||||
"--session-dir", str(session_dir),
|
||||
"--auto-approve", # agents run tools autonomously; no approval prompts
|
||||
]
|
||||
# Create-or-continue: the first send mints a session and records its id;
|
||||
# every later send with the same session_dir resumes that context window.
|
||||
if sid_file.exists():
|
||||
cmd += ["--resume", sid_file.read_text().strip()]
|
||||
if request.tools:
|
||||
cmd += ["--tools", ",".join(_translate_tools(request.tools))]
|
||||
for extension in request.extensions:
|
||||
cmd += ["-e", extension]
|
||||
cmd.append(request.prompt)
|
||||
|
||||
raw_path = Path(request.raw_output_path)
|
||||
raw_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
result = PiResult(session_id="", context_window=context_window(provider, model_id))
|
||||
# stdin is DEVNULL, deliberately. The prompt travels in argv, so the child
|
||||
# never needs stdin — but inheriting the parent's means omp sees a non-TTY
|
||||
# and can sit forever waiting for piped input that will never arrive or EOF.
|
||||
process = subprocess.Popen(cmd, stdin=subprocess.DEVNULL,
|
||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
|
||||
text=True, bufsize=1, cwd=request.cwd,
|
||||
env=operator_env())
|
||||
if on_spawn:
|
||||
on_spawn(process.pid)
|
||||
with raw_path.open("a") as raw:
|
||||
assert process.stdout is not None
|
||||
for line in process.stdout:
|
||||
raw.write(line)
|
||||
raw.flush() # events land on disk as they happen
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
try:
|
||||
event = json.loads(line)
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
if event.get("type") == "session":
|
||||
sid = event.get("id")
|
||||
if sid:
|
||||
result.session_id = sid
|
||||
sid_file.write_text(sid) # persist for later resumes
|
||||
if event.get("type") == "message_end":
|
||||
message = event.get("message", {})
|
||||
if message.get("role") == "assistant":
|
||||
text = _text_of(message)
|
||||
if text:
|
||||
result.text = text # last assistant message wins
|
||||
usage = message.get("usage", {}) or {}
|
||||
turn = _context_tokens(usage)
|
||||
result.tokens += turn
|
||||
result.usage.add_turn(usage, turn)
|
||||
# Occupancy is read off the last VALID assistant turn, the
|
||||
# way omp does it — an aborted or errored turn reports usage
|
||||
# you can't trust, so it must not overwrite a good reading.
|
||||
if turn and message.get("stopReason") not in ("aborted", "error"):
|
||||
result.context_tokens = turn
|
||||
result.cost += (usage.get("cost", {}) or {}).get("total", 0.0) or 0.0
|
||||
if on_event:
|
||||
on_event(event)
|
||||
|
||||
stderr = process.stderr.read() if process.stderr else ""
|
||||
result.returncode = process.wait()
|
||||
if on_exit:
|
||||
on_exit(process.pid)
|
||||
if result.returncode != 0 and not result.text:
|
||||
raise RuntimeError(f"omp exited {result.returncode}: {stderr.strip()[-800:]}")
|
||||
return result
|
||||
Loading…
Add table
Add a link
Reference in a new issue