axion-code 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.
- axion/__init__.py +3 -0
- axion/api/__init__.py +0 -0
- axion/api/anthropic.py +460 -0
- axion/api/client.py +259 -0
- axion/api/error.py +161 -0
- axion/api/ollama.py +597 -0
- axion/api/openai_compat.py +805 -0
- axion/api/openai_responses.py +627 -0
- axion/api/prompt_cache.py +31 -0
- axion/api/sse.py +98 -0
- axion/api/types.py +451 -0
- axion/cli/__init__.py +0 -0
- axion/cli/init_cmd.py +50 -0
- axion/cli/input.py +290 -0
- axion/cli/main.py +2953 -0
- axion/cli/render.py +489 -0
- axion/cli/tui.py +766 -0
- axion/commands/__init__.py +0 -0
- axion/commands/handlers/__init__.py +0 -0
- axion/commands/handlers/agents.py +51 -0
- axion/commands/handlers/builtin_commands.py +367 -0
- axion/commands/handlers/mcp.py +59 -0
- axion/commands/handlers/models.py +75 -0
- axion/commands/handlers/plugins.py +55 -0
- axion/commands/handlers/skills.py +61 -0
- axion/commands/parsing.py +317 -0
- axion/commands/registry.py +166 -0
- axion/compat_harness/__init__.py +0 -0
- axion/compat_harness/extractor.py +145 -0
- axion/plugins/__init__.py +0 -0
- axion/plugins/hooks.py +22 -0
- axion/plugins/manager.py +391 -0
- axion/plugins/manifest.py +270 -0
- axion/runtime/__init__.py +0 -0
- axion/runtime/bash.py +388 -0
- axion/runtime/bootstrap.py +39 -0
- axion/runtime/claude_subscription.py +300 -0
- axion/runtime/compact.py +233 -0
- axion/runtime/config.py +397 -0
- axion/runtime/conversation.py +1073 -0
- axion/runtime/file_ops.py +613 -0
- axion/runtime/git.py +213 -0
- axion/runtime/hooks.py +235 -0
- axion/runtime/image.py +212 -0
- axion/runtime/lanes.py +282 -0
- axion/runtime/lsp.py +425 -0
- axion/runtime/mcp/__init__.py +0 -0
- axion/runtime/mcp/client.py +76 -0
- axion/runtime/mcp/lifecycle.py +96 -0
- axion/runtime/mcp/stdio.py +318 -0
- axion/runtime/mcp/tool_bridge.py +79 -0
- axion/runtime/memory.py +196 -0
- axion/runtime/oauth.py +329 -0
- axion/runtime/openai_subscription.py +346 -0
- axion/runtime/permissions.py +247 -0
- axion/runtime/plan_mode.py +96 -0
- axion/runtime/policy_engine.py +259 -0
- axion/runtime/prompt.py +586 -0
- axion/runtime/recovery.py +261 -0
- axion/runtime/remote.py +28 -0
- axion/runtime/sandbox.py +68 -0
- axion/runtime/scheduler.py +231 -0
- axion/runtime/session.py +365 -0
- axion/runtime/sharing.py +159 -0
- axion/runtime/skills.py +124 -0
- axion/runtime/tasks.py +258 -0
- axion/runtime/usage.py +241 -0
- axion/runtime/workers.py +186 -0
- axion/telemetry/__init__.py +0 -0
- axion/telemetry/events.py +67 -0
- axion/telemetry/profile.py +49 -0
- axion/telemetry/sink.py +60 -0
- axion/telemetry/tracer.py +95 -0
- axion/tools/__init__.py +0 -0
- axion/tools/lane_completion.py +33 -0
- axion/tools/registry.py +853 -0
- axion/tools/tool_search.py +226 -0
- axion_code-1.0.0.dist-info/METADATA +709 -0
- axion_code-1.0.0.dist-info/RECORD +82 -0
- axion_code-1.0.0.dist-info/WHEEL +4 -0
- axion_code-1.0.0.dist-info/entry_points.txt +2 -0
- axion_code-1.0.0.dist-info/licenses/LICENSE +21 -0
axion/api/sse.py
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""Server-Sent Events (SSE) parser.
|
|
2
|
+
|
|
3
|
+
Maps to: rust/crates/api/src/sse.rs
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
|
|
10
|
+
from axion.api.error import InvalidSseFrameError
|
|
11
|
+
from axion.api.types import StreamEvent
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class SseParser:
|
|
15
|
+
"""Incremental SSE frame parser that handles chunked delivery."""
|
|
16
|
+
|
|
17
|
+
def __init__(self) -> None:
|
|
18
|
+
self._buffer = bytearray()
|
|
19
|
+
|
|
20
|
+
def push(self, chunk: bytes) -> list[StreamEvent]:
|
|
21
|
+
"""Push a chunk of data and return any complete events."""
|
|
22
|
+
self._buffer.extend(chunk)
|
|
23
|
+
events: list[StreamEvent] = []
|
|
24
|
+
|
|
25
|
+
while True:
|
|
26
|
+
frame = self._next_frame()
|
|
27
|
+
if frame is None:
|
|
28
|
+
break
|
|
29
|
+
event = parse_frame(frame)
|
|
30
|
+
if event is not None:
|
|
31
|
+
events.append(event)
|
|
32
|
+
|
|
33
|
+
return events
|
|
34
|
+
|
|
35
|
+
def finish(self) -> list[StreamEvent]:
|
|
36
|
+
"""Flush any remaining data in the buffer."""
|
|
37
|
+
if not self._buffer:
|
|
38
|
+
return []
|
|
39
|
+
|
|
40
|
+
trailing = self._buffer.decode("utf-8", errors="replace")
|
|
41
|
+
self._buffer.clear()
|
|
42
|
+
|
|
43
|
+
event = parse_frame(trailing)
|
|
44
|
+
return [event] if event is not None else []
|
|
45
|
+
|
|
46
|
+
def _next_frame(self) -> str | None:
|
|
47
|
+
"""Extract the next complete frame from the buffer."""
|
|
48
|
+
# Look for \n\n separator
|
|
49
|
+
pos = self._buffer.find(b"\n\n")
|
|
50
|
+
sep_len = 2
|
|
51
|
+
|
|
52
|
+
if pos == -1:
|
|
53
|
+
# Try \r\n\r\n
|
|
54
|
+
pos = self._buffer.find(b"\r\n\r\n")
|
|
55
|
+
sep_len = 4
|
|
56
|
+
|
|
57
|
+
if pos == -1:
|
|
58
|
+
return None
|
|
59
|
+
|
|
60
|
+
frame_bytes = bytes(self._buffer[: pos])
|
|
61
|
+
del self._buffer[: pos + sep_len]
|
|
62
|
+
return frame_bytes.decode("utf-8", errors="replace")
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def parse_frame(frame: str) -> StreamEvent | None:
|
|
66
|
+
"""Parse a single SSE frame into a StreamEvent."""
|
|
67
|
+
trimmed = frame.strip()
|
|
68
|
+
if not trimmed:
|
|
69
|
+
return None
|
|
70
|
+
|
|
71
|
+
data_lines: list[str] = []
|
|
72
|
+
event_name: str | None = None
|
|
73
|
+
|
|
74
|
+
for line in trimmed.splitlines():
|
|
75
|
+
if line.startswith(":"):
|
|
76
|
+
continue
|
|
77
|
+
if line.startswith("event:"):
|
|
78
|
+
event_name = line[len("event:"):].strip()
|
|
79
|
+
continue
|
|
80
|
+
if line.startswith("data:"):
|
|
81
|
+
data_lines.append(line[len("data:"):].lstrip())
|
|
82
|
+
|
|
83
|
+
if event_name == "ping":
|
|
84
|
+
return None
|
|
85
|
+
|
|
86
|
+
if not data_lines:
|
|
87
|
+
return None
|
|
88
|
+
|
|
89
|
+
payload = "\n".join(data_lines)
|
|
90
|
+
if payload == "[DONE]":
|
|
91
|
+
return None
|
|
92
|
+
|
|
93
|
+
try:
|
|
94
|
+
data = json.loads(payload)
|
|
95
|
+
except json.JSONDecodeError as exc:
|
|
96
|
+
raise InvalidSseFrameError(f"Invalid JSON in SSE data: {exc}") from exc
|
|
97
|
+
|
|
98
|
+
return StreamEvent.from_dict(data)
|
axion/api/types.py
ADDED
|
@@ -0,0 +1,451 @@
|
|
|
1
|
+
"""API data models for Anthropic and OpenAI-compatible providers.
|
|
2
|
+
|
|
3
|
+
Maps to: rust/crates/api/src/types.rs
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
from typing import Any, Literal
|
|
10
|
+
|
|
11
|
+
# ---------------------------------------------------------------------------
|
|
12
|
+
# Request types
|
|
13
|
+
# ---------------------------------------------------------------------------
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class MessageRequest:
|
|
17
|
+
model: str
|
|
18
|
+
max_tokens: int
|
|
19
|
+
messages: list[InputMessage]
|
|
20
|
+
system: str | None = None
|
|
21
|
+
tools: list[ToolDefinition] | None = None
|
|
22
|
+
tool_choice: ToolChoice | None = None
|
|
23
|
+
stream: bool = False
|
|
24
|
+
|
|
25
|
+
def with_streaming(self) -> MessageRequest:
|
|
26
|
+
self.stream = True
|
|
27
|
+
return self
|
|
28
|
+
|
|
29
|
+
def to_dict(self) -> dict[str, Any]:
|
|
30
|
+
d: dict[str, Any] = {
|
|
31
|
+
"model": self.model,
|
|
32
|
+
"max_tokens": self.max_tokens,
|
|
33
|
+
"messages": [m.to_dict() for m in self.messages],
|
|
34
|
+
}
|
|
35
|
+
if self.system is not None:
|
|
36
|
+
d["system"] = self.system
|
|
37
|
+
if self.tools is not None:
|
|
38
|
+
d["tools"] = [t.to_dict() for t in self.tools]
|
|
39
|
+
if self.tool_choice is not None:
|
|
40
|
+
d["tool_choice"] = self.tool_choice.to_dict()
|
|
41
|
+
if self.stream:
|
|
42
|
+
d["stream"] = True
|
|
43
|
+
return d
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass
|
|
47
|
+
class InputMessage:
|
|
48
|
+
role: str
|
|
49
|
+
content: list[InputContentBlock]
|
|
50
|
+
|
|
51
|
+
@classmethod
|
|
52
|
+
def user_text(cls, text: str) -> InputMessage:
|
|
53
|
+
return cls(role="user", content=[TextInputBlock(text=text)])
|
|
54
|
+
|
|
55
|
+
@classmethod
|
|
56
|
+
def user_tool_result(
|
|
57
|
+
cls, tool_use_id: str, content: str, is_error: bool = False
|
|
58
|
+
) -> InputMessage:
|
|
59
|
+
return cls(
|
|
60
|
+
role="user",
|
|
61
|
+
content=[
|
|
62
|
+
ToolResultBlock(
|
|
63
|
+
tool_use_id=tool_use_id,
|
|
64
|
+
content=[ToolResultTextContent(text=content)],
|
|
65
|
+
is_error=is_error,
|
|
66
|
+
)
|
|
67
|
+
],
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
def to_dict(self) -> dict[str, Any]:
|
|
71
|
+
return {
|
|
72
|
+
"role": self.role,
|
|
73
|
+
"content": [b.to_dict() for b in self.content],
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
# ---------------------------------------------------------------------------
|
|
78
|
+
# Input content blocks (tagged union via subclasses)
|
|
79
|
+
# ---------------------------------------------------------------------------
|
|
80
|
+
|
|
81
|
+
@dataclass
|
|
82
|
+
class InputContentBlock:
|
|
83
|
+
"""Base class for input content blocks."""
|
|
84
|
+
|
|
85
|
+
def to_dict(self) -> dict[str, Any]:
|
|
86
|
+
raise NotImplementedError
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@dataclass
|
|
90
|
+
class TextInputBlock(InputContentBlock):
|
|
91
|
+
text: str
|
|
92
|
+
|
|
93
|
+
def to_dict(self) -> dict[str, Any]:
|
|
94
|
+
return {"type": "text", "text": self.text}
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
@dataclass
|
|
98
|
+
class ToolUseInputBlock(InputContentBlock):
|
|
99
|
+
id: str
|
|
100
|
+
name: str
|
|
101
|
+
input: Any
|
|
102
|
+
|
|
103
|
+
def to_dict(self) -> dict[str, Any]:
|
|
104
|
+
return {"type": "tool_use", "id": self.id, "name": self.name, "input": self.input}
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@dataclass
|
|
108
|
+
class ImageInputBlock(InputContentBlock):
|
|
109
|
+
"""Image content block — base64-encoded image for vision models.
|
|
110
|
+
|
|
111
|
+
Works with both Anthropic (source.type=base64) and OpenAI (image_url).
|
|
112
|
+
"""
|
|
113
|
+
media_type: str # e.g. "image/png", "image/jpeg"
|
|
114
|
+
data: str # base64-encoded image data
|
|
115
|
+
|
|
116
|
+
def to_dict(self) -> dict[str, Any]:
|
|
117
|
+
# Anthropic format
|
|
118
|
+
return {
|
|
119
|
+
"type": "image",
|
|
120
|
+
"source": {
|
|
121
|
+
"type": "base64",
|
|
122
|
+
"media_type": self.media_type,
|
|
123
|
+
"data": self.data,
|
|
124
|
+
},
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
def to_openai_dict(self) -> dict[str, Any]:
|
|
128
|
+
"""OpenAI format for image_url with base64 data URL."""
|
|
129
|
+
return {
|
|
130
|
+
"type": "image_url",
|
|
131
|
+
"image_url": {
|
|
132
|
+
"url": f"data:{self.media_type};base64,{self.data}",
|
|
133
|
+
},
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
@dataclass
|
|
138
|
+
class ToolResultBlock(InputContentBlock):
|
|
139
|
+
tool_use_id: str
|
|
140
|
+
content: list[ToolResultContent]
|
|
141
|
+
is_error: bool = False
|
|
142
|
+
|
|
143
|
+
def to_dict(self) -> dict[str, Any]:
|
|
144
|
+
d: dict[str, Any] = {
|
|
145
|
+
"type": "tool_result",
|
|
146
|
+
"tool_use_id": self.tool_use_id,
|
|
147
|
+
"content": [c.to_dict() for c in self.content],
|
|
148
|
+
}
|
|
149
|
+
if self.is_error:
|
|
150
|
+
d["is_error"] = True
|
|
151
|
+
return d
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
# ---------------------------------------------------------------------------
|
|
155
|
+
# Tool result content blocks
|
|
156
|
+
# ---------------------------------------------------------------------------
|
|
157
|
+
|
|
158
|
+
@dataclass
|
|
159
|
+
class ToolResultContent:
|
|
160
|
+
def to_dict(self) -> dict[str, Any]:
|
|
161
|
+
raise NotImplementedError
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
@dataclass
|
|
165
|
+
class ToolResultTextContent(ToolResultContent):
|
|
166
|
+
text: str
|
|
167
|
+
|
|
168
|
+
def to_dict(self) -> dict[str, Any]:
|
|
169
|
+
return {"type": "text", "text": self.text}
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
@dataclass
|
|
173
|
+
class ToolResultJsonContent(ToolResultContent):
|
|
174
|
+
value: Any
|
|
175
|
+
|
|
176
|
+
def to_dict(self) -> dict[str, Any]:
|
|
177
|
+
return {"type": "json", "value": self.value}
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
# ---------------------------------------------------------------------------
|
|
181
|
+
# Tool definitions
|
|
182
|
+
# ---------------------------------------------------------------------------
|
|
183
|
+
|
|
184
|
+
@dataclass
|
|
185
|
+
class ToolDefinition:
|
|
186
|
+
name: str
|
|
187
|
+
input_schema: dict[str, Any]
|
|
188
|
+
description: str | None = None
|
|
189
|
+
|
|
190
|
+
def to_dict(self) -> dict[str, Any]:
|
|
191
|
+
d: dict[str, Any] = {"name": self.name, "input_schema": self.input_schema}
|
|
192
|
+
if self.description is not None:
|
|
193
|
+
d["description"] = self.description
|
|
194
|
+
return d
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
# ---------------------------------------------------------------------------
|
|
198
|
+
# Tool choice
|
|
199
|
+
# ---------------------------------------------------------------------------
|
|
200
|
+
|
|
201
|
+
@dataclass
|
|
202
|
+
class ToolChoice:
|
|
203
|
+
type: Literal["auto", "any", "tool"]
|
|
204
|
+
name: str | None = None
|
|
205
|
+
|
|
206
|
+
@classmethod
|
|
207
|
+
def auto(cls) -> ToolChoice:
|
|
208
|
+
return cls(type="auto")
|
|
209
|
+
|
|
210
|
+
@classmethod
|
|
211
|
+
def any(cls) -> ToolChoice:
|
|
212
|
+
return cls(type="any")
|
|
213
|
+
|
|
214
|
+
@classmethod
|
|
215
|
+
def tool(cls, name: str) -> ToolChoice:
|
|
216
|
+
return cls(type="tool", name=name)
|
|
217
|
+
|
|
218
|
+
def to_dict(self) -> dict[str, Any]:
|
|
219
|
+
d: dict[str, Any] = {"type": self.type}
|
|
220
|
+
if self.name is not None:
|
|
221
|
+
d["name"] = self.name
|
|
222
|
+
return d
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
# ---------------------------------------------------------------------------
|
|
226
|
+
# Response types
|
|
227
|
+
# ---------------------------------------------------------------------------
|
|
228
|
+
|
|
229
|
+
@dataclass
|
|
230
|
+
class MessageResponse:
|
|
231
|
+
id: str
|
|
232
|
+
type: str
|
|
233
|
+
role: str
|
|
234
|
+
content: list[OutputContentBlock]
|
|
235
|
+
model: str
|
|
236
|
+
usage: Usage
|
|
237
|
+
stop_reason: str | None = None
|
|
238
|
+
stop_sequence: str | None = None
|
|
239
|
+
request_id: str | None = None
|
|
240
|
+
|
|
241
|
+
def total_tokens(self) -> int:
|
|
242
|
+
return self.usage.total_tokens()
|
|
243
|
+
|
|
244
|
+
@classmethod
|
|
245
|
+
def from_dict(cls, data: dict[str, Any]) -> MessageResponse:
|
|
246
|
+
return cls(
|
|
247
|
+
id=data["id"],
|
|
248
|
+
type=data.get("type", "message"),
|
|
249
|
+
role=data.get("role", "assistant"),
|
|
250
|
+
content=[OutputContentBlock.from_dict(b) for b in data.get("content", [])],
|
|
251
|
+
model=data["model"],
|
|
252
|
+
usage=Usage.from_dict(data["usage"]),
|
|
253
|
+
stop_reason=data.get("stop_reason"),
|
|
254
|
+
stop_sequence=data.get("stop_sequence"),
|
|
255
|
+
request_id=data.get("request_id"),
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
# ---------------------------------------------------------------------------
|
|
260
|
+
# Output content blocks (tagged union via subclasses)
|
|
261
|
+
# ---------------------------------------------------------------------------
|
|
262
|
+
|
|
263
|
+
@dataclass
|
|
264
|
+
class OutputContentBlock:
|
|
265
|
+
"""Base class for output content blocks."""
|
|
266
|
+
|
|
267
|
+
@classmethod
|
|
268
|
+
def from_dict(cls, data: dict[str, Any]) -> OutputContentBlock:
|
|
269
|
+
block_type = data.get("type", "")
|
|
270
|
+
if block_type == "text":
|
|
271
|
+
return TextOutputBlock(text=data["text"])
|
|
272
|
+
if block_type == "tool_use":
|
|
273
|
+
return ToolUseOutputBlock(id=data["id"], name=data["name"], input=data["input"])
|
|
274
|
+
if block_type == "thinking":
|
|
275
|
+
return ThinkingBlock(thinking=data.get("thinking", ""), signature=data.get("signature"))
|
|
276
|
+
if block_type == "redacted_thinking":
|
|
277
|
+
return RedactedThinkingBlock(data=data.get("data"))
|
|
278
|
+
return TextOutputBlock(text=str(data))
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
@dataclass
|
|
282
|
+
class TextOutputBlock(OutputContentBlock):
|
|
283
|
+
text: str = ""
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
@dataclass
|
|
287
|
+
class ToolUseOutputBlock(OutputContentBlock):
|
|
288
|
+
id: str = ""
|
|
289
|
+
name: str = ""
|
|
290
|
+
input: Any = None
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
@dataclass
|
|
294
|
+
class ThinkingBlock(OutputContentBlock):
|
|
295
|
+
thinking: str = ""
|
|
296
|
+
signature: str | None = None
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
@dataclass
|
|
300
|
+
class RedactedThinkingBlock(OutputContentBlock):
|
|
301
|
+
data: Any = None
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
# ---------------------------------------------------------------------------
|
|
305
|
+
# Usage / tokens
|
|
306
|
+
# ---------------------------------------------------------------------------
|
|
307
|
+
|
|
308
|
+
@dataclass
|
|
309
|
+
class Usage:
|
|
310
|
+
input_tokens: int = 0
|
|
311
|
+
output_tokens: int = 0
|
|
312
|
+
cache_creation_input_tokens: int = 0
|
|
313
|
+
cache_read_input_tokens: int = 0
|
|
314
|
+
|
|
315
|
+
def total_tokens(self) -> int:
|
|
316
|
+
return (
|
|
317
|
+
self.input_tokens
|
|
318
|
+
+ self.output_tokens
|
|
319
|
+
+ self.cache_creation_input_tokens
|
|
320
|
+
+ self.cache_read_input_tokens
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
@classmethod
|
|
324
|
+
def from_dict(cls, data: dict[str, Any]) -> Usage:
|
|
325
|
+
return cls(
|
|
326
|
+
input_tokens=data.get("input_tokens", 0),
|
|
327
|
+
output_tokens=data.get("output_tokens", 0),
|
|
328
|
+
cache_creation_input_tokens=data.get("cache_creation_input_tokens", 0),
|
|
329
|
+
cache_read_input_tokens=data.get("cache_read_input_tokens", 0),
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
# ---------------------------------------------------------------------------
|
|
334
|
+
# Stream events (tagged union via subclasses)
|
|
335
|
+
# ---------------------------------------------------------------------------
|
|
336
|
+
|
|
337
|
+
@dataclass
|
|
338
|
+
class StreamEvent:
|
|
339
|
+
"""Base class for SSE stream events."""
|
|
340
|
+
|
|
341
|
+
@classmethod
|
|
342
|
+
def from_dict(cls, data: dict[str, Any]) -> StreamEvent:
|
|
343
|
+
event_type = data.get("type", "")
|
|
344
|
+
if event_type == "message_start":
|
|
345
|
+
return MessageStartEvent(
|
|
346
|
+
message=MessageResponse.from_dict(data["message"])
|
|
347
|
+
)
|
|
348
|
+
if event_type == "message_delta":
|
|
349
|
+
return MessageDeltaEvent(
|
|
350
|
+
delta=MessageDelta.from_dict(data["delta"]),
|
|
351
|
+
usage=Usage.from_dict(data["usage"]),
|
|
352
|
+
)
|
|
353
|
+
if event_type == "content_block_start":
|
|
354
|
+
return ContentBlockStartEvent(
|
|
355
|
+
index=data["index"],
|
|
356
|
+
content_block=OutputContentBlock.from_dict(data["content_block"]),
|
|
357
|
+
)
|
|
358
|
+
if event_type == "content_block_delta":
|
|
359
|
+
return ContentBlockDeltaEvent(
|
|
360
|
+
index=data["index"],
|
|
361
|
+
delta=ContentBlockDelta.from_dict(data["delta"]),
|
|
362
|
+
)
|
|
363
|
+
if event_type == "content_block_stop":
|
|
364
|
+
return ContentBlockStopEvent(index=data["index"])
|
|
365
|
+
if event_type == "message_stop":
|
|
366
|
+
return MessageStopEvent()
|
|
367
|
+
return StreamEvent()
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
@dataclass
|
|
371
|
+
class MessageStartEvent(StreamEvent):
|
|
372
|
+
message: MessageResponse | None = None
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
@dataclass
|
|
376
|
+
class MessageDelta:
|
|
377
|
+
stop_reason: str | None = None
|
|
378
|
+
stop_sequence: str | None = None
|
|
379
|
+
|
|
380
|
+
@classmethod
|
|
381
|
+
def from_dict(cls, data: dict[str, Any]) -> MessageDelta:
|
|
382
|
+
return cls(
|
|
383
|
+
stop_reason=data.get("stop_reason"),
|
|
384
|
+
stop_sequence=data.get("stop_sequence"),
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
@dataclass
|
|
389
|
+
class MessageDeltaEvent(StreamEvent):
|
|
390
|
+
delta: MessageDelta = field(default_factory=MessageDelta)
|
|
391
|
+
usage: Usage = field(default_factory=Usage)
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
@dataclass
|
|
395
|
+
class ContentBlockStartEvent(StreamEvent):
|
|
396
|
+
index: int = 0
|
|
397
|
+
content_block: OutputContentBlock | None = None
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
@dataclass
|
|
401
|
+
class ContentBlockDelta:
|
|
402
|
+
"""Base for content block deltas."""
|
|
403
|
+
|
|
404
|
+
@classmethod
|
|
405
|
+
def from_dict(cls, data: dict[str, Any]) -> ContentBlockDelta:
|
|
406
|
+
delta_type = data.get("type", "")
|
|
407
|
+
if delta_type == "text_delta":
|
|
408
|
+
return TextDelta(text=data.get("text", ""))
|
|
409
|
+
if delta_type == "input_json_delta":
|
|
410
|
+
return InputJsonDelta(partial_json=data.get("partial_json", ""))
|
|
411
|
+
if delta_type == "thinking_delta":
|
|
412
|
+
return ThinkingDelta(thinking=data.get("thinking", ""))
|
|
413
|
+
if delta_type == "signature_delta":
|
|
414
|
+
return SignatureDelta(signature=data.get("signature", ""))
|
|
415
|
+
return ContentBlockDelta()
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
@dataclass
|
|
419
|
+
class TextDelta(ContentBlockDelta):
|
|
420
|
+
text: str = ""
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
@dataclass
|
|
424
|
+
class InputJsonDelta(ContentBlockDelta):
|
|
425
|
+
partial_json: str = ""
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
@dataclass
|
|
429
|
+
class ThinkingDelta(ContentBlockDelta):
|
|
430
|
+
thinking: str = ""
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
@dataclass
|
|
434
|
+
class SignatureDelta(ContentBlockDelta):
|
|
435
|
+
signature: str = ""
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
@dataclass
|
|
439
|
+
class ContentBlockDeltaEvent(StreamEvent):
|
|
440
|
+
index: int = 0
|
|
441
|
+
delta: ContentBlockDelta = field(default_factory=ContentBlockDelta)
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
@dataclass
|
|
445
|
+
class ContentBlockStopEvent(StreamEvent):
|
|
446
|
+
index: int = 0
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
@dataclass
|
|
450
|
+
class MessageStopEvent(StreamEvent):
|
|
451
|
+
pass
|
axion/cli/__init__.py
ADDED
|
File without changes
|
axion/cli/init_cmd.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""Repository initialization command — creates AXION.md."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from rich.console import Console
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def init_project(cwd: Path | None = None, console: Console | None = None) -> None:
|
|
11
|
+
"""Initialize a new project with AXION.md and .axion/ directory."""
|
|
12
|
+
actual_cwd = cwd or Path.cwd()
|
|
13
|
+
con = console or Console()
|
|
14
|
+
|
|
15
|
+
# Check for existing instruction files
|
|
16
|
+
axion_md = actual_cwd / "AXION.md"
|
|
17
|
+
claude_md = actual_cwd / "CLAUDE.md"
|
|
18
|
+
|
|
19
|
+
if axion_md.exists():
|
|
20
|
+
con.print("[yellow]AXION.md already exists[/yellow]")
|
|
21
|
+
return
|
|
22
|
+
if claude_md.exists():
|
|
23
|
+
con.print("[yellow]CLAUDE.md found — rename to AXION.md? (Axion reads both)[/yellow]")
|
|
24
|
+
return
|
|
25
|
+
|
|
26
|
+
# Create .axion directory
|
|
27
|
+
axion_dir = actual_cwd / ".axion"
|
|
28
|
+
axion_dir.mkdir(exist_ok=True)
|
|
29
|
+
|
|
30
|
+
# Create AXION.md
|
|
31
|
+
axion_md.write_text(
|
|
32
|
+
"# AXION.md\n\n"
|
|
33
|
+
"This file provides guidance to Axion Code when working with this codebase.\n\n"
|
|
34
|
+
"## Project overview\n\n"
|
|
35
|
+
"<!-- Describe your project here -->\n\n"
|
|
36
|
+
"## Build & test\n\n"
|
|
37
|
+
"<!-- Add build and test commands -->\n\n"
|
|
38
|
+
"## Code conventions\n\n"
|
|
39
|
+
"<!-- Add coding style guidelines -->\n",
|
|
40
|
+
encoding="utf-8",
|
|
41
|
+
)
|
|
42
|
+
con.print("[green]Created AXION.md[/green]")
|
|
43
|
+
|
|
44
|
+
# Create .axion/settings.json if it doesn't exist
|
|
45
|
+
settings = axion_dir / "settings.json"
|
|
46
|
+
if not settings.exists():
|
|
47
|
+
settings.write_text(
|
|
48
|
+
'{\n "permissions": {\n "defaultMode": "prompt"\n }\n}\n'
|
|
49
|
+
)
|
|
50
|
+
con.print("[green]Created .axion/settings.json[/green]")
|