voidx 1.0.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 (126) hide show
  1. voidx/__init__.py +3 -0
  2. voidx/agent/__init__.py +0 -0
  3. voidx/agent/agents.py +439 -0
  4. voidx/agent/attachments.py +235 -0
  5. voidx/agent/graph.py +463 -0
  6. voidx/agent/graph_components/__init__.py +1 -0
  7. voidx/agent/graph_components/compaction.py +268 -0
  8. voidx/agent/graph_components/permissions.py +139 -0
  9. voidx/agent/graph_components/run_loop.py +532 -0
  10. voidx/agent/graph_components/runtime.py +14 -0
  11. voidx/agent/graph_components/streaming.py +351 -0
  12. voidx/agent/graph_components/subagent.py +278 -0
  13. voidx/agent/graph_components/tool_execution.py +208 -0
  14. voidx/agent/runtime_context.py +368 -0
  15. voidx/agent/slash.py +466 -0
  16. voidx/agent/slash_components/__init__.py +1 -0
  17. voidx/agent/slash_components/code_ide.py +68 -0
  18. voidx/agent/slash_components/lsp.py +105 -0
  19. voidx/agent/slash_components/mcp.py +332 -0
  20. voidx/agent/slash_components/model.py +419 -0
  21. voidx/agent/slash_components/runtime.py +55 -0
  22. voidx/agent/slash_components/skills.py +94 -0
  23. voidx/agent/state.py +32 -0
  24. voidx/agent/task_state.py +278 -0
  25. voidx/agent/tool_filters.py +27 -0
  26. voidx/config.py +707 -0
  27. voidx/llm/__init__.py +0 -0
  28. voidx/llm/catalog.py +188 -0
  29. voidx/llm/compaction.py +267 -0
  30. voidx/llm/context.py +43 -0
  31. voidx/llm/instruction.py +220 -0
  32. voidx/llm/provider.py +312 -0
  33. voidx/llm/usage.py +341 -0
  34. voidx/lsp/__init__.py +30 -0
  35. voidx/lsp/client.py +259 -0
  36. voidx/lsp/config.py +172 -0
  37. voidx/lsp/detector.py +512 -0
  38. voidx/lsp/errors.py +19 -0
  39. voidx/lsp/manager.py +280 -0
  40. voidx/lsp/schema.py +179 -0
  41. voidx/lsp/service.py +103 -0
  42. voidx/main.py +154 -0
  43. voidx/mcp/__init__.py +33 -0
  44. voidx/mcp/client.py +458 -0
  45. voidx/mcp/manager.py +267 -0
  46. voidx/mcp/schema.py +112 -0
  47. voidx/mcp/tool.py +122 -0
  48. voidx/mcp_servers/__init__.py +1 -0
  49. voidx/mcp_servers/web.py +104 -0
  50. voidx/memory/__init__.py +0 -0
  51. voidx/memory/context_frames.py +188 -0
  52. voidx/memory/model_profiles.py +98 -0
  53. voidx/memory/runtime_state.py +240 -0
  54. voidx/memory/session.py +272 -0
  55. voidx/memory/store.py +245 -0
  56. voidx/memory/transcript.py +137 -0
  57. voidx/permission/__init__.py +28 -0
  58. voidx/permission/engine.py +430 -0
  59. voidx/permission/evaluate.py +114 -0
  60. voidx/permission/sandbox.py +280 -0
  61. voidx/permission/schema.py +24 -0
  62. voidx/permission/service.py +314 -0
  63. voidx/permission/wildcard.py +34 -0
  64. voidx/skills/__init__.py +18 -0
  65. voidx/skills/bundled/superpowers/receiving-code-review/SKILL.md +30 -0
  66. voidx/skills/bundled/superpowers/requesting-code-review/SKILL.md +27 -0
  67. voidx/skills/bundled/superpowers/systematic-debugging/SKILL.md +36 -0
  68. voidx/skills/bundled/superpowers/test-driven-development/SKILL.md +33 -0
  69. voidx/skills/bundled/superpowers/verification-before-completion/SKILL.md +31 -0
  70. voidx/skills/bundled/superpowers/writing-plans/SKILL.md +27 -0
  71. voidx/skills/policy.py +97 -0
  72. voidx/skills/registry.py +162 -0
  73. voidx/skills/schema.py +47 -0
  74. voidx/skills/service.py +199 -0
  75. voidx/tools/__init__.py +0 -0
  76. voidx/tools/agent.py +81 -0
  77. voidx/tools/base.py +86 -0
  78. voidx/tools/bash.py +105 -0
  79. voidx/tools/file_ops.py +193 -0
  80. voidx/tools/lsp.py +155 -0
  81. voidx/tools/registry.py +104 -0
  82. voidx/tools/repomap.py +238 -0
  83. voidx/tools/search.py +162 -0
  84. voidx/tools/task_status.py +57 -0
  85. voidx/tools/task_tracker.py +81 -0
  86. voidx/tools/todo.py +82 -0
  87. voidx/tools/web_content.py +357 -0
  88. voidx/tools/web_mcp.py +107 -0
  89. voidx/tools/webfetch.py +155 -0
  90. voidx/tools/websearch.py +276 -0
  91. voidx/ui/__init__.py +0 -0
  92. voidx/ui/app.py +1033 -0
  93. voidx/ui/app_components/__init__.py +1 -0
  94. voidx/ui/app_components/clipboard_image.py +245 -0
  95. voidx/ui/app_components/commands.py +18 -0
  96. voidx/ui/app_components/controls.py +29 -0
  97. voidx/ui/app_components/file_picker.py +115 -0
  98. voidx/ui/app_components/formatting.py +187 -0
  99. voidx/ui/app_components/git_changes.py +51 -0
  100. voidx/ui/app_components/rendering.py +1169 -0
  101. voidx/ui/browse.py +160 -0
  102. voidx/ui/capture.py +169 -0
  103. voidx/ui/code_ide.py +251 -0
  104. voidx/ui/commands.py +83 -0
  105. voidx/ui/console.py +381 -0
  106. voidx/ui/console_components/__init__.py +1 -0
  107. voidx/ui/console_components/formatting.py +96 -0
  108. voidx/ui/console_components/streaming.py +253 -0
  109. voidx/ui/diff.py +331 -0
  110. voidx/ui/dock.py +372 -0
  111. voidx/ui/dock_components/__init__.py +1 -0
  112. voidx/ui/dock_components/formatting.py +123 -0
  113. voidx/ui/dock_components/nodes.py +401 -0
  114. voidx/ui/dock_components/state.py +51 -0
  115. voidx/ui/event_components/__init__.py +1 -0
  116. voidx/ui/event_components/schema.py +249 -0
  117. voidx/ui/events.py +341 -0
  118. voidx/ui/session_changes.py +163 -0
  119. voidx/ui/startup.py +161 -0
  120. voidx/ui/transcript.py +148 -0
  121. voidx/ui/tree.py +316 -0
  122. voidx-1.0.0.dist-info/METADATA +59 -0
  123. voidx-1.0.0.dist-info/RECORD +126 -0
  124. voidx-1.0.0.dist-info/WHEEL +5 -0
  125. voidx-1.0.0.dist-info/entry_points.txt +2 -0
  126. voidx-1.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,30 @@
1
+ ---
2
+ name: receiving-code-review
3
+ description: Use when receiving review feedback, requested optimizations, or reviewer comments before implementing them.
4
+ triggers:
5
+ - review feedback
6
+ - code review feedback
7
+ - reviewer says
8
+ - feedback says
9
+ - review comment
10
+ - 优化点
11
+ - 审查意见
12
+ - 评审意见
13
+ ---
14
+
15
+ # Receiving Code Review for voidx
16
+
17
+ Use this skill when the user or another reviewer gives feedback to implement.
18
+
19
+ Core rule: verify feedback against the codebase before changing code.
20
+
21
+ Workflow:
22
+ 1. Read the full feedback.
23
+ 2. Restate the concrete requested changes if needed.
24
+ 3. Check the relevant code and tests.
25
+ 4. Decide whether each item is correct for this codebase.
26
+ 5. Push back with technical reasons when feedback is wrong or unnecessary.
27
+ 6. If feedback is valid, implement one coherent item at a time.
28
+ 7. Verify with targeted tests or commands before reporting.
29
+
30
+ Do not give performative agreement. Technical correctness and the user's intent decide what changes.
@@ -0,0 +1,27 @@
1
+ ---
2
+ name: requesting-code-review
3
+ description: Use after substantial implementation work, complex bug fixes, or before merging to request a focused review.
4
+ triggers:
5
+ - request review
6
+ - ask for review
7
+ - before merge
8
+ - pre-merge
9
+ - review this change
10
+ - 复核一下
11
+ - 合并前
12
+ ---
13
+
14
+ # Requesting Code Review for voidx
15
+
16
+ Use this skill after substantial implementation work, complex bug fixes, or before merge.
17
+
18
+ In voidx, request review with `agent(review)` when available.
19
+
20
+ Review brief should include:
21
+ 1. What changed.
22
+ 2. Requirements or plan being checked.
23
+ 3. Files changed or relevant diff range.
24
+ 4. Verification already run.
25
+ 5. Specific risks to inspect.
26
+
27
+ Act on review findings by severity. Fix correctness, security, and broken behavior before proceeding. Push back if the review is wrong and explain the evidence.
@@ -0,0 +1,36 @@
1
+ ---
2
+ name: systematic-debugging
3
+ description: Use when debugging bugs, failed tests, build failures, tracebacks, crashes, or unexpected behavior.
4
+ triggers:
5
+ - bug
6
+ - failed
7
+ - failure
8
+ - traceback
9
+ - error
10
+ - crash
11
+ - broken
12
+ - test failure
13
+ - build failure
14
+ - 报错
15
+ - 失败
16
+ - 异常
17
+ - 崩溃
18
+ - 排查
19
+ ---
20
+
21
+ # Systematic Debugging for voidx
22
+
23
+ Use this skill before proposing or applying fixes for bugs, failed tests, build failures, crashes, or unexpected behavior.
24
+
25
+ Core rule: find the root cause before changing code.
26
+
27
+ Workflow:
28
+ 1. Read the full error, traceback, logs, or failing assertion.
29
+ 2. Reproduce the issue with the smallest reliable command or steps.
30
+ 3. Check recent changes with read-only tools such as `grep`, `read`, and safe `bash`.
31
+ 4. Form one concrete hypothesis from evidence.
32
+ 5. Verify the hypothesis with a targeted command, diagnostic, or code read.
33
+ 6. Only then make the smallest fix.
34
+ 7. Run the reproduction command again and report the evidence.
35
+
36
+ For flaky or non-reproducible failures, gather more evidence instead of guessing.
@@ -0,0 +1,33 @@
1
+ ---
2
+ name: test-driven-development
3
+ description: Use before implementing features, bug fixes, refactors, or behavior changes.
4
+ triggers:
5
+ - implement
6
+ - feature
7
+ - bugfix
8
+ - refactor
9
+ - behavior change
10
+ - add support
11
+ - fix bug
12
+ - 实现
13
+ - 修复
14
+ - 重构
15
+ - 功能
16
+ ---
17
+
18
+ # Test-Driven Development for voidx
19
+
20
+ Use this skill before writing production code for a feature, bug fix, refactor, or behavior change.
21
+
22
+ Core rule: write a test that fails for the intended reason before writing the implementation.
23
+
24
+ Workflow:
25
+ 1. Identify the smallest behavior to prove.
26
+ 2. Add or update a focused test.
27
+ 3. Run the targeted test and confirm it fails for the expected reason.
28
+ 4. Implement the smallest code change that makes the test pass.
29
+ 5. Run the targeted test again.
30
+ 6. Refactor only after the test is green.
31
+ 7. Run the relevant broader test set before reporting completion.
32
+
33
+ Allowed exceptions: pure documentation, prompt-only edits, generated assets, or configuration-only changes. If you skip TDD for one of these, say why briefly.
@@ -0,0 +1,31 @@
1
+ ---
2
+ name: verification-before-completion
3
+ description: Use before claiming work is complete, fixed, passing, ready, or safe to merge.
4
+ triggers:
5
+ - done
6
+ - complete
7
+ - fixed
8
+ - passing
9
+ - ready
10
+ - verify
11
+ - verified
12
+ - 完成
13
+ - 修好了
14
+ - 通过
15
+ - 验证
16
+ ---
17
+
18
+ # Verification Before Completion for voidx
19
+
20
+ Use this skill before saying work is complete, fixed, passing, ready, or safe to merge.
21
+
22
+ Core rule: evidence before completion claims.
23
+
24
+ Checklist:
25
+ 1. Identify the command or check that proves the claim.
26
+ 2. Run the full relevant command in this turn.
27
+ 3. Read the exit code and failure count.
28
+ 4. If the check fails, report the actual failure and next step.
29
+ 5. If the check passes, report the command and result.
30
+
31
+ Do not rely on earlier runs, assumptions, or subagent summaries without verifying the actual state.
@@ -0,0 +1,27 @@
1
+ ---
2
+ name: writing-plans
3
+ description: Use when turning a spec, requirements, or agreed design into an implementation plan before editing code.
4
+ triggers:
5
+ - implementation plan
6
+ - write a plan
7
+ - planning
8
+ - spec
9
+ - requirements
10
+ - 计划
11
+ - 实施方案
12
+ - 需求
13
+ ---
14
+
15
+ # Writing Plans for voidx
16
+
17
+ Use this skill when the user has a spec, requirements, or agreed design and wants an implementation plan.
18
+
19
+ Plan structure:
20
+ 1. Goal: one sentence.
21
+ 2. Context: current code paths and constraints.
22
+ 3. Files: exact files to create or modify.
23
+ 4. Tasks: small ordered steps with checkboxes.
24
+ 5. Tests: targeted commands and expected results.
25
+ 6. Risks: edge cases, compatibility, and rollback concerns.
26
+
27
+ Keep plans executable by voidx: use exact paths, concrete commands, and voidx tool names. Do not force git worktrees or commits unless the user asked for them.
voidx/skills/policy.py ADDED
@@ -0,0 +1,97 @@
1
+ """Workflow skill activation policy."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+
7
+
8
+ @dataclass(frozen=True)
9
+ class WorkflowSkillActivation:
10
+ name: str
11
+ reason: str
12
+
13
+
14
+ WORKFLOW_SKILL_PRIORITY = {
15
+ "systematic-debugging": 10,
16
+ "receiving-code-review": 20,
17
+ "writing-plans": 30,
18
+ "test-driven-development": 40,
19
+ "verification-before-completion": 50,
20
+ "requesting-code-review": 60,
21
+ }
22
+
23
+
24
+ def workflow_skill_activations(
25
+ user_text: str,
26
+ *,
27
+ agent: str = "",
28
+ task_intent: str | None = None,
29
+ interaction_mode: str | None = None,
30
+ ) -> list[WorkflowSkillActivation]:
31
+ text = user_text.strip().lower()
32
+ agent_name = (agent or "").strip().lower()
33
+ intent = (task_intent or "").strip().lower()
34
+ mode = (interaction_mode or "").strip().lower()
35
+ activations: dict[str, WorkflowSkillActivation] = {}
36
+
37
+ def add(name: str, reason: str) -> None:
38
+ activations.setdefault(name, WorkflowSkillActivation(name=name, reason=reason))
39
+
40
+ if intent == "debug":
41
+ add("systematic-debugging", "debug intent")
42
+ add("verification-before-completion", "debug lifecycle")
43
+
44
+ if agent_name == "implement":
45
+ add("test-driven-development", "implement role")
46
+ add("verification-before-completion", "implement lifecycle")
47
+ elif intent == "implement":
48
+ add("test-driven-development", "implement intent")
49
+ add("verification-before-completion", "implement lifecycle")
50
+
51
+ if agent_name == "plan":
52
+ add("writing-plans", "plan role")
53
+
54
+ if intent == "review" and _contains_any(text, _REVIEW_FEEDBACK_TERMS):
55
+ add("receiving-code-review", "review feedback")
56
+
57
+ if intent == "design" and _contains_any(text, _PLAN_TERMS):
58
+ add("writing-plans", "planning intent")
59
+
60
+ if mode == "plan":
61
+ add("writing-plans", "plan mode")
62
+
63
+ return sorted(
64
+ activations.values(),
65
+ key=lambda item: (WORKFLOW_SKILL_PRIORITY.get(item.name, 999), item.name),
66
+ )
67
+
68
+
69
+ def workflow_skill_sort_key(name: str) -> tuple[int, str]:
70
+ return (WORKFLOW_SKILL_PRIORITY.get(name, 999), name)
71
+
72
+
73
+ def _contains_any(text: str, terms: tuple[str, ...]) -> bool:
74
+ return any(term in text for term in terms)
75
+
76
+
77
+ _REVIEW_FEEDBACK_TERMS = (
78
+ "review feedback",
79
+ "code review feedback",
80
+ "review comment",
81
+ "reviewer says",
82
+ "feedback says",
83
+ "优化点",
84
+ "审查意见",
85
+ "评审意见",
86
+ )
87
+
88
+ _PLAN_TERMS = (
89
+ "implementation plan",
90
+ "write a plan",
91
+ "planning",
92
+ "spec",
93
+ "requirements",
94
+ "计划",
95
+ "实施方案",
96
+ "需求",
97
+ )
@@ -0,0 +1,162 @@
1
+ """Local skill discovery from SKILL.md files."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+ from voidx.skills.schema import SkillDefinition, SkillMeta, SkillScope
9
+
10
+ SKILL_FILENAME = "SKILL.md"
11
+
12
+
13
+ def normalize_skill_name(name: str) -> str:
14
+ return name.strip().lower()
15
+
16
+
17
+ class SkillParseError(ValueError):
18
+ pass
19
+
20
+
21
+ class SkillRegistry:
22
+ """Discovers bundled, global, and project skills.
23
+
24
+ Search order is bundled first, then global, then project. Later sources
25
+ override earlier sources with the same skill name.
26
+ """
27
+
28
+ def __init__(
29
+ self,
30
+ workspace: str,
31
+ *,
32
+ bundled_dir: Path | None = None,
33
+ global_dir: Path | None = None,
34
+ project_dir: Path | None = None,
35
+ ) -> None:
36
+ self.workspace = Path(workspace).resolve()
37
+ self.bundled_dir = bundled_dir or (Path(__file__).resolve().parent / "bundled" / "superpowers")
38
+ self.global_dir = global_dir or (Path.home() / ".voidx" / "skills")
39
+ self.project_dir = project_dir or (self.workspace / ".voidx" / "skills")
40
+
41
+ def discover(self) -> list[SkillDefinition]:
42
+ skills: dict[str, SkillDefinition] = {}
43
+ for scope, root in (
44
+ ("bundled", self.bundled_dir),
45
+ ("global", self.global_dir),
46
+ ("project", self.project_dir),
47
+ ):
48
+ for skill in self._discover_root(root, scope):
49
+ skills[normalize_skill_name(skill.name)] = skill
50
+ return sorted(skills.values(), key=lambda item: item.name)
51
+
52
+ def get(self, name: str) -> SkillDefinition | None:
53
+ target = normalize_skill_name(name)
54
+ for skill in self.discover():
55
+ if normalize_skill_name(skill.name) == target:
56
+ return skill
57
+ return None
58
+
59
+ def _discover_root(self, root: Path, scope: SkillScope) -> list[SkillDefinition]:
60
+ if not root.exists() or not root.is_dir():
61
+ return []
62
+ skills: list[SkillDefinition] = []
63
+ for skill_file in sorted(root.glob(f"*/{SKILL_FILENAME}")):
64
+ try:
65
+ skills.append(parse_skill_file(skill_file, scope=scope))
66
+ except SkillParseError:
67
+ continue
68
+ return skills
69
+
70
+
71
+ def parse_skill_file(path: Path, *, scope: SkillScope) -> SkillDefinition:
72
+ text = path.read_text(encoding="utf-8", errors="replace")
73
+ fields, body = _split_frontmatter(text)
74
+ name = str(fields.get("name") or path.parent.name).strip()
75
+ if not name:
76
+ raise SkillParseError(f"Skill at {path} has no name")
77
+ meta = SkillMeta(
78
+ name=name,
79
+ description=str(fields.get("description") or ""),
80
+ enabled=_coerce_bool(fields.get("enabled"), default=True),
81
+ triggers=[str(item).strip() for item in _coerce_list(fields.get("triggers")) if str(item).strip()],
82
+ scope=scope,
83
+ )
84
+ return SkillDefinition(meta=meta, path=path.resolve(), body=body.strip())
85
+
86
+
87
+ def _split_frontmatter(text: str) -> tuple[dict[str, Any], str]:
88
+ lines = text.splitlines()
89
+ if not lines or lines[0].strip() != "---":
90
+ return {}, text
91
+
92
+ end_index = None
93
+ for index, line in enumerate(lines[1:], start=1):
94
+ if line.strip() == "---":
95
+ end_index = index
96
+ break
97
+ if end_index is None:
98
+ raise SkillParseError("Unclosed frontmatter")
99
+
100
+ frontmatter = "\n".join(lines[1:end_index])
101
+ body = "\n".join(lines[end_index + 1:])
102
+ return _parse_frontmatter(frontmatter), body
103
+
104
+
105
+ def _parse_frontmatter(text: str) -> dict[str, Any]:
106
+ result: dict[str, Any] = {}
107
+ current_key: str | None = None
108
+ for raw_line in text.splitlines():
109
+ line = raw_line.rstrip()
110
+ stripped = line.strip()
111
+ if not stripped or stripped.startswith("#"):
112
+ continue
113
+ if stripped.startswith("- ") and current_key:
114
+ current = result.setdefault(current_key, [])
115
+ if isinstance(current, list):
116
+ current.append(_parse_scalar(stripped[2:].strip()))
117
+ continue
118
+ if ":" not in line:
119
+ continue
120
+ key, value = line.split(":", 1)
121
+ current_key = key.strip()
122
+ value = value.strip()
123
+ if not current_key:
124
+ continue
125
+ if value == "":
126
+ result[current_key] = []
127
+ else:
128
+ result[current_key] = _parse_scalar(value)
129
+ return result
130
+
131
+
132
+ def _parse_scalar(value: str) -> Any:
133
+ value = value.strip()
134
+ if len(value) >= 2 and value[0] == value[-1] and value[0] in ("'", '"'):
135
+ value = value[1:-1]
136
+ lower = value.lower()
137
+ if lower in {"true", "yes", "on"}:
138
+ return True
139
+ if lower in {"false", "no", "off"}:
140
+ return False
141
+ if value.startswith("[") and value.endswith("]"):
142
+ inner = value[1:-1].strip()
143
+ if not inner:
144
+ return []
145
+ return [_parse_scalar(part.strip()) for part in inner.split(",")]
146
+ return value
147
+
148
+
149
+ def _coerce_bool(value: Any, *, default: bool) -> bool:
150
+ if value is None:
151
+ return default
152
+ if isinstance(value, bool):
153
+ return value
154
+ return str(value).strip().lower() not in {"false", "no", "off", "0"}
155
+
156
+
157
+ def _coerce_list(value: Any) -> list[Any]:
158
+ if value is None:
159
+ return []
160
+ if isinstance(value, list):
161
+ return value
162
+ return [value]
voidx/skills/schema.py ADDED
@@ -0,0 +1,47 @@
1
+ """Types for local skill discovery and selection."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ from typing import Literal
7
+
8
+ from pydantic import BaseModel, Field
9
+
10
+
11
+ SkillScope = Literal["bundled", "global", "project"]
12
+
13
+
14
+ class SkillMeta(BaseModel):
15
+ name: str
16
+ description: str = ""
17
+ enabled: bool = True
18
+ triggers: list[str] = Field(default_factory=list)
19
+ scope: SkillScope = "project"
20
+
21
+
22
+ class SkillDefinition(BaseModel):
23
+ meta: SkillMeta
24
+ path: Path
25
+ body: str
26
+
27
+ @property
28
+ def name(self) -> str:
29
+ return self.meta.name
30
+
31
+ @property
32
+ def source_dir(self) -> Path:
33
+ return self.path.parent
34
+
35
+
36
+ class SkillSelectionConfig(BaseModel):
37
+ enabled: set[str] = Field(default_factory=set)
38
+ disabled: set[str] = Field(default_factory=set)
39
+
40
+
41
+ class SkillMatch(BaseModel):
42
+ skill: SkillDefinition
43
+ reason: str
44
+
45
+ @property
46
+ def name(self) -> str:
47
+ return self.skill.name
@@ -0,0 +1,199 @@
1
+ """Runtime skill selection and instruction rendering."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from collections.abc import Iterable
7
+
8
+ from voidx.skills.policy import workflow_skill_activations, workflow_skill_sort_key
9
+ from voidx.skills.registry import SkillRegistry, normalize_skill_name
10
+ from voidx.skills.schema import SkillDefinition, SkillMatch, SkillSelectionConfig
11
+
12
+ _EXPLICIT_REF_RE = re.compile(r"(?<![\w.-])\$([A-Za-z0-9_.-]+)")
13
+
14
+
15
+ class SkillService:
16
+ def __init__(
17
+ self,
18
+ registry: SkillRegistry,
19
+ *,
20
+ selection: SkillSelectionConfig | None = None,
21
+ ) -> None:
22
+ self._registry = registry
23
+ self._selection = selection or SkillSelectionConfig()
24
+
25
+ def list_skills(self) -> list[SkillDefinition]:
26
+ return self._registry.discover()
27
+
28
+ def get(self, name: str) -> SkillDefinition | None:
29
+ return self._registry.get(name)
30
+
31
+ def enabled_skills(self) -> list[SkillDefinition]:
32
+ return [skill for skill in self.list_skills() if self.is_enabled(skill)]
33
+
34
+ def is_enabled(self, skill: SkillDefinition) -> bool:
35
+ name = normalize_skill_name(skill.name)
36
+ if name in self._normalized(self._selection.disabled):
37
+ return False
38
+ if name in self._normalized(self._selection.enabled):
39
+ return True
40
+ return skill.meta.enabled
41
+
42
+ def select(
43
+ self,
44
+ user_text: str,
45
+ *,
46
+ agent: str = "",
47
+ task_intent: str | None = None,
48
+ interaction_mode: str | None = None,
49
+ limit: int = 5,
50
+ ) -> list[SkillMatch]:
51
+ text = user_text.strip()
52
+ has_context = bool(agent or task_intent or interaction_mode)
53
+ if not text and not has_context:
54
+ return []
55
+
56
+ skills = self.enabled_skills()
57
+ skills_by_name = {normalize_skill_name(skill.name): skill for skill in skills}
58
+ explicit = self._explicit_refs(text)
59
+ matches: list[SkillMatch] = []
60
+ seen: set[str] = set()
61
+
62
+ def add_match(skill: SkillDefinition | None, reason: str) -> None:
63
+ if skill is None:
64
+ return
65
+ name = normalize_skill_name(skill.name)
66
+ if name in seen:
67
+ return
68
+ seen.add(name)
69
+ matches.append(SkillMatch(skill=skill, reason=reason))
70
+
71
+ if explicit:
72
+ for name in sorted(explicit, key=workflow_skill_sort_key):
73
+ add_match(skills_by_name.get(name), "explicit")
74
+ for activation in workflow_skill_activations(
75
+ text,
76
+ agent=agent,
77
+ task_intent=task_intent,
78
+ interaction_mode=interaction_mode,
79
+ ):
80
+ add_match(skills_by_name.get(normalize_skill_name(activation.name)), activation.reason)
81
+ return matches[:limit]
82
+
83
+ for activation in workflow_skill_activations(
84
+ text,
85
+ agent=agent,
86
+ task_intent=task_intent,
87
+ interaction_mode=interaction_mode,
88
+ ):
89
+ add_match(skills_by_name.get(normalize_skill_name(activation.name)), activation.reason)
90
+
91
+ text_matches: list[SkillMatch] = []
92
+ lowered = text.lower()
93
+ for skill in skills:
94
+ if normalize_skill_name(skill.name) in seen:
95
+ continue
96
+ reason = self._match_reason(skill, lowered)
97
+ if reason:
98
+ text_matches.append(SkillMatch(skill=skill, reason=reason))
99
+ text_matches.sort(key=lambda match: workflow_skill_sort_key(match.name))
100
+ matches.extend(text_matches)
101
+ return matches[:limit]
102
+
103
+ def activation_summaries(
104
+ self,
105
+ user_text: str,
106
+ *,
107
+ agent: str = "",
108
+ task_intent: str | None = None,
109
+ interaction_mode: str | None = None,
110
+ limit: int = 5,
111
+ ) -> list[str]:
112
+ return [
113
+ f"{match.name} ({match.reason})"
114
+ for match in self.select(
115
+ user_text,
116
+ agent=agent,
117
+ task_intent=task_intent,
118
+ interaction_mode=interaction_mode,
119
+ limit=limit,
120
+ )
121
+ ]
122
+
123
+ def instructions_for(
124
+ self,
125
+ user_text: str,
126
+ *,
127
+ agent: str = "",
128
+ task_intent: str | None = None,
129
+ interaction_mode: str | None = None,
130
+ limit: int = 5,
131
+ ) -> list[str]:
132
+ return [
133
+ self.render_instruction(match.skill)
134
+ for match in self.select(
135
+ user_text,
136
+ agent=agent,
137
+ task_intent=task_intent,
138
+ interaction_mode=interaction_mode,
139
+ limit=limit,
140
+ )
141
+ ]
142
+
143
+ @staticmethod
144
+ def render_instruction(skill: SkillDefinition) -> str:
145
+ description = skill.meta.description.strip()
146
+ header = f"Skill instructions from: {skill.path}\nSkill: {skill.name}"
147
+ if description:
148
+ header += f"\nDescription: {description}"
149
+ return f"{header}\n\n{skill.body}".strip()
150
+
151
+ @staticmethod
152
+ def _normalized(values: Iterable[str]) -> set[str]:
153
+ return {normalize_skill_name(value) for value in values}
154
+
155
+ @staticmethod
156
+ def _explicit_refs(text: str) -> set[str]:
157
+ return {normalize_skill_name(match.group(1)) for match in _EXPLICIT_REF_RE.finditer(text)}
158
+
159
+ @staticmethod
160
+ def _match_reason(skill: SkillDefinition, lowered_text: str) -> str:
161
+ name = normalize_skill_name(skill.name)
162
+ if _contains_phrase(lowered_text, name):
163
+ return "name"
164
+
165
+ for trigger in skill.meta.triggers:
166
+ normalized = trigger.strip().lower()
167
+ if normalized and normalized in lowered_text:
168
+ return f"trigger:{trigger}"
169
+
170
+ description_terms = _significant_terms(skill.meta.description)
171
+ if description_terms and sum(1 for term in description_terms if term in lowered_text) >= 2:
172
+ return "description"
173
+ return ""
174
+
175
+
176
+ def _contains_phrase(text: str, phrase: str) -> bool:
177
+ if not phrase:
178
+ return False
179
+ pattern = r"(?<![\w.-])" + re.escape(phrase) + r"(?![\w.-])"
180
+ return bool(re.search(pattern, text))
181
+
182
+
183
+ def _significant_terms(text: str) -> set[str]:
184
+ stop = {
185
+ "with",
186
+ "from",
187
+ "that",
188
+ "this",
189
+ "when",
190
+ "into",
191
+ "using",
192
+ "skill",
193
+ "skills",
194
+ }
195
+ return {
196
+ term
197
+ for term in re.findall(r"[A-Za-z][A-Za-z0-9_-]{3,}", text.lower())
198
+ if term not in stop
199
+ }
File without changes