subagent-cli 0.1.1__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.
- subagent/__init__.py +7 -0
- subagent/acp_client.py +366 -0
- subagent/approval_utils.py +57 -0
- subagent/cli.py +1125 -0
- subagent/config.py +305 -0
- subagent/constants.py +21 -0
- subagent/controller_service.py +267 -0
- subagent/daemon.py +133 -0
- subagent/errors.py +24 -0
- subagent/handoff_service.py +354 -0
- subagent/hints.py +36 -0
- subagent/input_contract.py +121 -0
- subagent/launcher_service.py +30 -0
- subagent/output.py +41 -0
- subagent/paths.py +63 -0
- subagent/prompt_service.py +114 -0
- subagent/runtime_service.py +342 -0
- subagent/simple_yaml.py +202 -0
- subagent/state.py +1049 -0
- subagent/turn_service.py +558 -0
- subagent/worker_runtime.py +758 -0
- subagent/worker_service.py +362 -0
- subagent_cli-0.1.1.dist-info/METADATA +98 -0
- subagent_cli-0.1.1.dist-info/RECORD +27 -0
- subagent_cli-0.1.1.dist-info/WHEEL +4 -0
- subagent_cli-0.1.1.dist-info/entry_points.txt +3 -0
- subagent_cli-0.1.1.dist-info/licenses/LICENSE +21 -0
subagent/simple_yaml.py
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
"""Very small YAML subset parser for offline bootstrap.
|
|
2
|
+
|
|
3
|
+
This intentionally supports only the subset used by subagent v1 config:
|
|
4
|
+
- mappings via `key: value` and nested indentation
|
|
5
|
+
- lists via `- item`
|
|
6
|
+
- block scalar literal via `|` for multi-line strings
|
|
7
|
+
- scalar values: string, int, float, bool, null, `{}`, `[]`
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass(slots=True)
|
|
17
|
+
class Line:
|
|
18
|
+
indent: int
|
|
19
|
+
content: str
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ParseError(ValueError):
|
|
23
|
+
pass
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def parse_yaml_subset(text: str) -> Any:
|
|
27
|
+
lines = _tokenize(text)
|
|
28
|
+
if not lines:
|
|
29
|
+
return {}
|
|
30
|
+
parser = _Parser(lines)
|
|
31
|
+
value = parser.parse_block(0)
|
|
32
|
+
if parser.has_next():
|
|
33
|
+
raise ParseError("Unexpected trailing content")
|
|
34
|
+
return value if value is not None else {}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _tokenize(text: str) -> list[Line]:
|
|
38
|
+
out: list[Line] = []
|
|
39
|
+
for raw in text.splitlines():
|
|
40
|
+
if "\t" in raw:
|
|
41
|
+
raise ParseError("Tab indentation is not supported")
|
|
42
|
+
stripped_comments = _strip_inline_comment(raw)
|
|
43
|
+
if not stripped_comments.strip():
|
|
44
|
+
continue
|
|
45
|
+
indent = len(stripped_comments) - len(stripped_comments.lstrip(" "))
|
|
46
|
+
out.append(Line(indent=indent, content=stripped_comments.strip()))
|
|
47
|
+
return out
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _strip_inline_comment(line: str) -> str:
|
|
51
|
+
in_single = False
|
|
52
|
+
in_double = False
|
|
53
|
+
for idx, char in enumerate(line):
|
|
54
|
+
if char == "'" and not in_double:
|
|
55
|
+
in_single = not in_single
|
|
56
|
+
continue
|
|
57
|
+
if char == '"' and not in_single:
|
|
58
|
+
in_double = not in_double
|
|
59
|
+
continue
|
|
60
|
+
if char == "#" and not in_single and not in_double:
|
|
61
|
+
if idx == 0 or line[idx - 1].isspace():
|
|
62
|
+
return line[:idx].rstrip()
|
|
63
|
+
return line.rstrip()
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class _Parser:
|
|
67
|
+
def __init__(self, lines: list[Line]) -> None:
|
|
68
|
+
self.lines = lines
|
|
69
|
+
self.index = 0
|
|
70
|
+
|
|
71
|
+
def has_next(self) -> bool:
|
|
72
|
+
return self.index < len(self.lines)
|
|
73
|
+
|
|
74
|
+
def peek(self) -> Line | None:
|
|
75
|
+
if not self.has_next():
|
|
76
|
+
return None
|
|
77
|
+
return self.lines[self.index]
|
|
78
|
+
|
|
79
|
+
def next_line(self) -> Line:
|
|
80
|
+
line = self.lines[self.index]
|
|
81
|
+
self.index += 1
|
|
82
|
+
return line
|
|
83
|
+
|
|
84
|
+
def parse_block(self, indent: int) -> Any:
|
|
85
|
+
line = self.peek()
|
|
86
|
+
if line is None or line.indent < indent:
|
|
87
|
+
return None
|
|
88
|
+
if line.indent > indent:
|
|
89
|
+
raise ParseError(f"Unexpected indentation at line {self.index + 1}")
|
|
90
|
+
if line.content.startswith("- "):
|
|
91
|
+
return self.parse_list(indent)
|
|
92
|
+
return self.parse_mapping(indent)
|
|
93
|
+
|
|
94
|
+
def parse_mapping(self, indent: int) -> dict[str, Any]:
|
|
95
|
+
mapping: dict[str, Any] = {}
|
|
96
|
+
while self.has_next():
|
|
97
|
+
line = self.peek()
|
|
98
|
+
assert line is not None
|
|
99
|
+
if line.indent < indent:
|
|
100
|
+
break
|
|
101
|
+
if line.indent > indent:
|
|
102
|
+
raise ParseError(f"Unexpected indentation at line {self.index + 1}")
|
|
103
|
+
if line.content.startswith("- "):
|
|
104
|
+
break
|
|
105
|
+
|
|
106
|
+
current = self.next_line()
|
|
107
|
+
if ":" not in current.content:
|
|
108
|
+
raise ParseError(f"Expected key:value mapping at line {self.index}")
|
|
109
|
+
key, rest = current.content.split(":", 1)
|
|
110
|
+
key = key.strip()
|
|
111
|
+
rest = rest.strip()
|
|
112
|
+
if not key:
|
|
113
|
+
raise ParseError(f"Empty key at line {self.index}")
|
|
114
|
+
|
|
115
|
+
if rest == "":
|
|
116
|
+
nested = self.parse_block(indent + 2)
|
|
117
|
+
mapping[key] = {} if nested is None else nested
|
|
118
|
+
elif rest == "|":
|
|
119
|
+
mapping[key] = self.parse_literal(indent + 2)
|
|
120
|
+
else:
|
|
121
|
+
mapping[key] = _parse_scalar(rest)
|
|
122
|
+
return mapping
|
|
123
|
+
|
|
124
|
+
def parse_list(self, indent: int) -> list[Any]:
|
|
125
|
+
items: list[Any] = []
|
|
126
|
+
while self.has_next():
|
|
127
|
+
line = self.peek()
|
|
128
|
+
assert line is not None
|
|
129
|
+
if line.indent < indent:
|
|
130
|
+
break
|
|
131
|
+
if line.indent > indent:
|
|
132
|
+
raise ParseError(f"Unexpected indentation at line {self.index + 1}")
|
|
133
|
+
if not line.content.startswith("- "):
|
|
134
|
+
break
|
|
135
|
+
|
|
136
|
+
current = self.next_line()
|
|
137
|
+
body = current.content[2:].strip()
|
|
138
|
+
if body == "":
|
|
139
|
+
nested = self.parse_block(indent + 2)
|
|
140
|
+
items.append({} if nested is None else nested)
|
|
141
|
+
elif ":" in body and not body.startswith(("'", '"')):
|
|
142
|
+
key, rest = body.split(":", 1)
|
|
143
|
+
key = key.strip()
|
|
144
|
+
rest = rest.strip()
|
|
145
|
+
value: Any
|
|
146
|
+
if rest == "":
|
|
147
|
+
nested = self.parse_block(indent + 2)
|
|
148
|
+
value = {} if nested is None else nested
|
|
149
|
+
elif rest == "|":
|
|
150
|
+
value = self.parse_literal(indent + 2)
|
|
151
|
+
else:
|
|
152
|
+
value = _parse_scalar(rest)
|
|
153
|
+
items.append({key: value})
|
|
154
|
+
else:
|
|
155
|
+
items.append(_parse_scalar(body))
|
|
156
|
+
return items
|
|
157
|
+
|
|
158
|
+
def parse_literal(self, indent: int) -> str:
|
|
159
|
+
fragments: list[str] = []
|
|
160
|
+
while self.has_next():
|
|
161
|
+
line = self.peek()
|
|
162
|
+
assert line is not None
|
|
163
|
+
if line.indent < indent:
|
|
164
|
+
break
|
|
165
|
+
current = self.next_line()
|
|
166
|
+
# Preserve relative indentation in block scalar.
|
|
167
|
+
slice_start = min(indent, current.indent)
|
|
168
|
+
reconstructed = (" " * (current.indent - slice_start)) + current.content
|
|
169
|
+
fragments.append(reconstructed)
|
|
170
|
+
return "\n".join(fragments).rstrip()
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _parse_scalar(value: str) -> Any:
|
|
174
|
+
lowered = value.lower()
|
|
175
|
+
if lowered in {"null", "~"}:
|
|
176
|
+
return None
|
|
177
|
+
if lowered == "true":
|
|
178
|
+
return True
|
|
179
|
+
if lowered == "false":
|
|
180
|
+
return False
|
|
181
|
+
if value == "{}":
|
|
182
|
+
return {}
|
|
183
|
+
if value == "[]":
|
|
184
|
+
return []
|
|
185
|
+
if value.startswith('"') and value.endswith('"') and len(value) >= 2:
|
|
186
|
+
return value[1:-1]
|
|
187
|
+
if value.startswith("'") and value.endswith("'") and len(value) >= 2:
|
|
188
|
+
return value[1:-1]
|
|
189
|
+
|
|
190
|
+
try:
|
|
191
|
+
if value.startswith("0") and value != "0":
|
|
192
|
+
raise ValueError
|
|
193
|
+
return int(value)
|
|
194
|
+
except ValueError:
|
|
195
|
+
pass
|
|
196
|
+
|
|
197
|
+
try:
|
|
198
|
+
return float(value)
|
|
199
|
+
except ValueError:
|
|
200
|
+
pass
|
|
201
|
+
|
|
202
|
+
return value
|