minion-code 0.1.0__py3-none-any.whl → 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.
Files changed (115) hide show
  1. examples/cli_entrypoint.py +60 -0
  2. examples/{agent_with_todos.py → components/agent_with_todos.py} +58 -47
  3. examples/{message_response_children_demo.py → components/message_response_children_demo.py} +61 -55
  4. examples/components/messages_component.py +199 -0
  5. examples/file_freshness_example.py +22 -22
  6. examples/file_watching_example.py +32 -26
  7. examples/interruptible_tui.py +921 -3
  8. examples/repl_tui.py +129 -0
  9. examples/skills/example_usage.py +57 -0
  10. examples/start.py +173 -0
  11. minion_code/__init__.py +1 -1
  12. minion_code/acp_server/__init__.py +34 -0
  13. minion_code/acp_server/agent.py +539 -0
  14. minion_code/acp_server/hooks.py +354 -0
  15. minion_code/acp_server/main.py +194 -0
  16. minion_code/acp_server/permissions.py +142 -0
  17. minion_code/acp_server/test_client.py +104 -0
  18. minion_code/adapters/__init__.py +22 -0
  19. minion_code/adapters/output_adapter.py +207 -0
  20. minion_code/adapters/rich_adapter.py +169 -0
  21. minion_code/adapters/textual_adapter.py +254 -0
  22. minion_code/agents/__init__.py +2 -2
  23. minion_code/agents/code_agent.py +517 -104
  24. minion_code/agents/hooks.py +378 -0
  25. minion_code/cli.py +538 -429
  26. minion_code/cli_simple.py +665 -0
  27. minion_code/commands/__init__.py +136 -29
  28. minion_code/commands/clear_command.py +19 -46
  29. minion_code/commands/help_command.py +33 -49
  30. minion_code/commands/history_command.py +37 -55
  31. minion_code/commands/model_command.py +194 -0
  32. minion_code/commands/quit_command.py +9 -12
  33. minion_code/commands/resume_command.py +181 -0
  34. minion_code/commands/skill_command.py +89 -0
  35. minion_code/commands/status_command.py +48 -73
  36. minion_code/commands/tools_command.py +54 -52
  37. minion_code/commands/version_command.py +34 -69
  38. minion_code/components/ConfirmDialog.py +430 -0
  39. minion_code/components/Message.py +318 -97
  40. minion_code/components/MessageResponse.py +30 -29
  41. minion_code/components/Messages.py +351 -0
  42. minion_code/components/PromptInput.py +499 -245
  43. minion_code/components/__init__.py +24 -17
  44. minion_code/const.py +7 -0
  45. minion_code/screens/REPL.py +1453 -469
  46. minion_code/screens/__init__.py +1 -1
  47. minion_code/services/__init__.py +20 -20
  48. minion_code/services/event_system.py +19 -14
  49. minion_code/services/file_freshness_service.py +223 -170
  50. minion_code/skills/__init__.py +25 -0
  51. minion_code/skills/skill.py +128 -0
  52. minion_code/skills/skill_loader.py +198 -0
  53. minion_code/skills/skill_registry.py +177 -0
  54. minion_code/subagents/__init__.py +31 -0
  55. minion_code/subagents/builtin/__init__.py +30 -0
  56. minion_code/subagents/builtin/claude_code_guide.py +32 -0
  57. minion_code/subagents/builtin/explore.py +36 -0
  58. minion_code/subagents/builtin/general_purpose.py +19 -0
  59. minion_code/subagents/builtin/plan.py +61 -0
  60. minion_code/subagents/subagent.py +116 -0
  61. minion_code/subagents/subagent_loader.py +147 -0
  62. minion_code/subagents/subagent_registry.py +151 -0
  63. minion_code/tools/__init__.py +8 -2
  64. minion_code/tools/bash_tool.py +16 -3
  65. minion_code/tools/file_edit_tool.py +201 -104
  66. minion_code/tools/file_read_tool.py +183 -26
  67. minion_code/tools/file_write_tool.py +17 -3
  68. minion_code/tools/glob_tool.py +23 -2
  69. minion_code/tools/grep_tool.py +229 -21
  70. minion_code/tools/ls_tool.py +28 -3
  71. minion_code/tools/multi_edit_tool.py +89 -84
  72. minion_code/tools/python_interpreter_tool.py +9 -1
  73. minion_code/tools/skill_tool.py +210 -0
  74. minion_code/tools/task_tool.py +287 -0
  75. minion_code/tools/todo_read_tool.py +28 -24
  76. minion_code/tools/todo_write_tool.py +82 -65
  77. minion_code/{types.py → type_defs.py} +15 -2
  78. minion_code/utils/__init__.py +45 -17
  79. minion_code/utils/config.py +610 -0
  80. minion_code/utils/history.py +114 -0
  81. minion_code/utils/logs.py +53 -0
  82. minion_code/utils/mcp_loader.py +153 -55
  83. minion_code/utils/output_truncator.py +233 -0
  84. minion_code/utils/session_storage.py +369 -0
  85. minion_code/utils/todo_file_utils.py +26 -22
  86. minion_code/utils/todo_storage.py +43 -33
  87. minion_code/web/__init__.py +9 -0
  88. minion_code/web/adapters/__init__.py +5 -0
  89. minion_code/web/adapters/web_adapter.py +524 -0
  90. minion_code/web/api/__init__.py +7 -0
  91. minion_code/web/api/chat.py +277 -0
  92. minion_code/web/api/interactions.py +136 -0
  93. minion_code/web/api/sessions.py +135 -0
  94. minion_code/web/server.py +149 -0
  95. minion_code/web/services/__init__.py +5 -0
  96. minion_code/web/services/session_manager.py +420 -0
  97. minion_code-0.1.1.dist-info/METADATA +475 -0
  98. minion_code-0.1.1.dist-info/RECORD +111 -0
  99. {minion_code-0.1.0.dist-info → minion_code-0.1.1.dist-info}/WHEEL +1 -1
  100. minion_code-0.1.1.dist-info/entry_points.txt +6 -0
  101. tests/test_adapter.py +67 -0
  102. tests/test_adapter_simple.py +79 -0
  103. tests/test_file_read_tool.py +144 -0
  104. tests/test_readonly_tools.py +0 -2
  105. tests/test_skills.py +441 -0
  106. examples/advance_tui.py +0 -508
  107. examples/rich_example.py +0 -4
  108. examples/simple_file_watching.py +0 -57
  109. examples/simple_tui.py +0 -267
  110. examples/simple_usage.py +0 -69
  111. minion_code-0.1.0.dist-info/METADATA +0 -350
  112. minion_code-0.1.0.dist-info/RECORD +0 -59
  113. minion_code-0.1.0.dist-info/entry_points.txt +0 -4
  114. {minion_code-0.1.0.dist-info → minion_code-0.1.1.dist-info}/licenses/LICENSE +0 -0
  115. {minion_code-0.1.0.dist-info → minion_code-0.1.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,277 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ Chat API endpoint with SSE streaming.
5
+
6
+ Handles chat messages and returns streaming responses via Server-Sent Events.
7
+ """
8
+
9
+ import asyncio
10
+ import json
11
+ import logging
12
+ from typing import Optional, AsyncGenerator
13
+
14
+ from fastapi import APIRouter, HTTPException, Request
15
+ from fastapi.responses import StreamingResponse
16
+ from pydantic import BaseModel, Field
17
+
18
+ from ..services.session_manager import session_manager, HistoryMode
19
+ from ..adapters.web_adapter import TaskState, SSEEvent
20
+
21
+ logger = logging.getLogger(__name__)
22
+
23
+ router = APIRouter(prefix="/api/chat", tags=["chat"])
24
+
25
+
26
+ class ChatRequest(BaseModel):
27
+ """Request body for chat endpoint."""
28
+
29
+ message: str = Field(..., description="User message content")
30
+ history_mode: Optional[HistoryMode] = Field(
31
+ default=None, description="Override session's history mode for this request"
32
+ )
33
+
34
+
35
+ def format_sse_event(event: SSEEvent) -> str:
36
+ """Format event as SSE string."""
37
+ data = json.dumps(event.to_dict(), ensure_ascii=False)
38
+ return f"data: {data}\n\n"
39
+
40
+
41
+ def format_sse_done() -> str:
42
+ """Format done event."""
43
+ return "data: [DONE]\n\n"
44
+
45
+
46
+ async def process_chat_stream(
47
+ session_id: str, message: str, history_mode: Optional[HistoryMode] = None
48
+ ) -> AsyncGenerator[str, None]:
49
+ """
50
+ Process chat message and yield SSE events.
51
+
52
+ This is the core streaming logic:
53
+ 1. Get or create session
54
+ 2. Create task ID and update adapter
55
+ 3. Get agent (based on history mode)
56
+ 4. Run agent and forward events
57
+ 5. Save messages to storage
58
+ """
59
+ # Get session
60
+ session = await session_manager.get_session(session_id)
61
+ if not session:
62
+ yield format_sse_event(
63
+ SSEEvent(
64
+ type="error",
65
+ data={"message": "Session not found", "code": "SESSION_NOT_FOUND"},
66
+ )
67
+ )
68
+ yield format_sse_done()
69
+ return
70
+
71
+ # Use request history_mode or session default
72
+ effective_history_mode = history_mode or session.history_mode
73
+
74
+ # Generate task ID
75
+ task_id = session.generate_task_id()
76
+ session.current_task_id = task_id
77
+ session.adapter.set_task_id(task_id)
78
+
79
+ # Reset abort event
80
+ session.abort_event.clear()
81
+
82
+ try:
83
+ # Emit task started
84
+ await session.adapter.emit_task_status(TaskState.SUBMITTED)
85
+ yield format_sse_event(
86
+ SSEEvent(
87
+ type="task_status",
88
+ data={"state": TaskState.SUBMITTED.value, "task_id": task_id},
89
+ task_id=task_id,
90
+ )
91
+ )
92
+
93
+ # Get or create agent
94
+ agent = await session_manager.get_or_create_agent(session)
95
+
96
+ # Emit working status
97
+ await session.adapter.emit_task_status(TaskState.WORKING)
98
+ yield format_sse_event(
99
+ SSEEvent(
100
+ type="task_status",
101
+ data={"state": TaskState.WORKING.value, "task_id": task_id},
102
+ task_id=task_id,
103
+ )
104
+ )
105
+
106
+ # Save user message
107
+ session_manager.save_message(session, "user", message)
108
+
109
+ # Run agent with streaming
110
+ full_response = ""
111
+
112
+ # Create concurrent tasks for agent execution and event forwarding
113
+ async def run_agent():
114
+ nonlocal full_response
115
+ async for chunk in agent.run_async(message, stream=True):
116
+ # Check for abort
117
+ if session.abort_event.is_set():
118
+ break
119
+
120
+ chunk_type = getattr(chunk, "chunk_type", "text")
121
+ chunk_content = getattr(chunk, "content", str(chunk))
122
+ chunk_metadata = getattr(chunk, "metadata", {})
123
+
124
+ if chunk_type == "step_start":
125
+ await session.adapter._emit_event(
126
+ "step_start", {"content": chunk_content}
127
+ )
128
+ elif chunk_type == "thinking":
129
+ await session.adapter.emit_thinking(chunk_content)
130
+ elif chunk_type == "code_start":
131
+ await session.adapter._emit_event(
132
+ "code_start",
133
+ {
134
+ "code": chunk_content,
135
+ "language": chunk_metadata.get("language", ""),
136
+ },
137
+ )
138
+ elif chunk_type == "code_result":
139
+ await session.adapter.emit_tool_result(
140
+ success=chunk_metadata.get("success", True),
141
+ output=chunk_content,
142
+ )
143
+ elif chunk_type == "tool_call":
144
+ await session.adapter.emit_tool_call(
145
+ name=chunk_metadata.get("tool_name", ""),
146
+ args=chunk_metadata.get("args", {}),
147
+ )
148
+ elif chunk_type in ("final_answer", "agent_response", "completion"):
149
+ final_content = (
150
+ getattr(chunk, "answer", chunk_content) or chunk_content
151
+ )
152
+ full_response = str(final_content)
153
+ await session.adapter.emit_content(full_response)
154
+ else:
155
+ # Default: treat as content
156
+ if chunk_content:
157
+ await session.adapter.emit_content(chunk_content)
158
+
159
+ # Start agent execution in background
160
+ agent_task = asyncio.create_task(run_agent())
161
+
162
+ # Forward events from adapter queue to SSE
163
+ try:
164
+ while True:
165
+ try:
166
+ # Try to get event with timeout
167
+ event = await asyncio.wait_for(
168
+ session.adapter.event_queue.get(), timeout=0.1
169
+ )
170
+ yield format_sse_event(event)
171
+ except asyncio.TimeoutError:
172
+ pass
173
+
174
+ # Check if agent is done
175
+ if agent_task.done():
176
+ # Drain remaining events
177
+ while not session.adapter.event_queue.empty():
178
+ event = await session.adapter.event_queue.get()
179
+ yield format_sse_event(event)
180
+
181
+ # Check for exception
182
+ if agent_task.exception():
183
+ raise agent_task.exception()
184
+
185
+ break
186
+
187
+ # Check for abort
188
+ if session.abort_event.is_set():
189
+ agent_task.cancel()
190
+ yield format_sse_event(
191
+ SSEEvent(
192
+ type="task_status",
193
+ data={
194
+ "state": TaskState.CANCELLED.value,
195
+ "task_id": task_id,
196
+ },
197
+ task_id=task_id,
198
+ )
199
+ )
200
+ break
201
+
202
+ except Exception as e:
203
+ logger.error(f"Error in event forwarding: {e}")
204
+ raise
205
+
206
+ # Save assistant response
207
+ if full_response:
208
+ session_manager.save_message(session, "assistant", full_response)
209
+
210
+ # Emit completed status
211
+ await session.adapter.emit_task_status(TaskState.COMPLETED)
212
+ yield format_sse_event(
213
+ SSEEvent(
214
+ type="task_status",
215
+ data={"state": TaskState.COMPLETED.value, "task_id": task_id},
216
+ task_id=task_id,
217
+ )
218
+ )
219
+
220
+ except asyncio.CancelledError:
221
+ yield format_sse_event(
222
+ SSEEvent(
223
+ type="task_status",
224
+ data={"state": TaskState.CANCELLED.value, "task_id": task_id},
225
+ task_id=task_id,
226
+ )
227
+ )
228
+ except Exception as e:
229
+ logger.exception(f"Error processing chat: {e}")
230
+ await session.adapter.emit_error(str(e))
231
+ yield format_sse_event(
232
+ SSEEvent(type="error", data={"message": str(e)}, task_id=task_id)
233
+ )
234
+ await session.adapter.emit_task_status(TaskState.FAILED)
235
+ yield format_sse_event(
236
+ SSEEvent(
237
+ type="task_status",
238
+ data={"state": TaskState.FAILED.value, "task_id": task_id},
239
+ task_id=task_id,
240
+ )
241
+ )
242
+ finally:
243
+ session.current_task_id = None
244
+ yield format_sse_done()
245
+
246
+
247
+ @router.post("/{session_id}")
248
+ async def chat(session_id: str, request: ChatRequest):
249
+ """
250
+ Send a chat message and receive streaming response.
251
+
252
+ Returns a Server-Sent Events stream with the following event types:
253
+ - task_status: Task state changes (submitted, working, input_required, completed, failed)
254
+ - content: Streaming text content
255
+ - thinking: LLM reasoning content
256
+ - tool_call: Tool invocation
257
+ - tool_result: Tool execution result
258
+ - input_required: Request for user input (permission, text, choice)
259
+ - error: Error message
260
+ - done: Stream end marker ([DONE])
261
+
262
+ For input_required events, respond via POST /api/tasks/{task_id}/input
263
+ """
264
+ # Validate session exists
265
+ session = await session_manager.get_session(session_id)
266
+ if not session:
267
+ raise HTTPException(status_code=404, detail="Session not found")
268
+
269
+ return StreamingResponse(
270
+ process_chat_stream(session_id, request.message, request.history_mode),
271
+ media_type="text/event-stream",
272
+ headers={
273
+ "Cache-Control": "no-cache",
274
+ "Connection": "keep-alive",
275
+ "X-Accel-Buffering": "no", # Disable nginx buffering
276
+ },
277
+ )
@@ -0,0 +1,136 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ Interactions API endpoint.
5
+
6
+ Handles user responses to input_required events (permission, text input, choice).
7
+ """
8
+
9
+ from fastapi import APIRouter, HTTPException
10
+ from pydantic import BaseModel, Field
11
+ from typing import Union, Optional, Any
12
+
13
+ from ..services.session_manager import session_manager
14
+
15
+ router = APIRouter(prefix="/api/tasks", tags=["interactions"])
16
+
17
+
18
+ class InteractionResponse(BaseModel):
19
+ """Request body for responding to an interaction."""
20
+
21
+ interaction_id: str = Field(
22
+ ..., description="Interaction ID from input_required event"
23
+ )
24
+ response: Any = Field(
25
+ ...,
26
+ description="User's response: bool for permission, int for choice, str for text",
27
+ )
28
+
29
+
30
+ class InteractionResult(BaseModel):
31
+ """Response after processing interaction."""
32
+
33
+ status: str
34
+ interaction_id: str
35
+ task_id: Optional[str] = None
36
+
37
+
38
+ @router.post("/{task_id}/input", response_model=InteractionResult)
39
+ async def respond_to_interaction(task_id: str, body: InteractionResponse):
40
+ """
41
+ Respond to an input_required event.
42
+
43
+ This endpoint is called when the user responds to a permission request,
44
+ makes a choice, or provides text input.
45
+
46
+ The response type depends on the interaction kind:
47
+ - permission: bool (true = allow, false = deny)
48
+ - choice: int (selected index, -1 = cancelled)
49
+ - text: str or null (null = cancelled)
50
+
51
+ Example:
52
+ POST /api/tasks/task_123/input
53
+ {
54
+ "interaction_id": "int_456",
55
+ "response": true
56
+ }
57
+ """
58
+ # Find session containing this interaction
59
+ session = session_manager.find_session_by_interaction(body.interaction_id)
60
+ if not session:
61
+ raise HTTPException(
62
+ status_code=404, detail=f"Interaction {body.interaction_id} not found"
63
+ )
64
+
65
+ # Verify task_id matches (optional validation)
66
+ if session.current_task_id and session.current_task_id != task_id:
67
+ raise HTTPException(
68
+ status_code=400,
69
+ detail=f"Task ID mismatch: expected {session.current_task_id}, got {task_id}",
70
+ )
71
+
72
+ # Resolve the interaction
73
+ resolved = session.adapter.resolve_interaction(body.interaction_id, body.response)
74
+ if not resolved:
75
+ raise HTTPException(
76
+ status_code=400,
77
+ detail=f"Interaction {body.interaction_id} already resolved or not found",
78
+ )
79
+
80
+ return InteractionResult(
81
+ status="ok", interaction_id=body.interaction_id, task_id=task_id
82
+ )
83
+
84
+
85
+ @router.post("/{task_id}/cancel")
86
+ async def cancel_interaction(task_id: str, interaction_id: str):
87
+ """
88
+ Cancel a pending interaction.
89
+
90
+ Sets appropriate default value based on interaction type:
91
+ - permission: false (denied)
92
+ - choice: -1 (cancelled)
93
+ - text: null (cancelled)
94
+ """
95
+ session = session_manager.find_session_by_interaction(interaction_id)
96
+ if not session:
97
+ raise HTTPException(
98
+ status_code=404, detail=f"Interaction {interaction_id} not found"
99
+ )
100
+
101
+ cancelled = session.adapter.cancel_interaction(interaction_id)
102
+ if not cancelled:
103
+ raise HTTPException(
104
+ status_code=400,
105
+ detail=f"Interaction {interaction_id} already resolved or not found",
106
+ )
107
+
108
+ return {"status": "cancelled", "interaction_id": interaction_id, "task_id": task_id}
109
+
110
+
111
+ # Alternative endpoint path for convenience
112
+ @router.post("/input/{interaction_id}")
113
+ async def respond_to_interaction_by_id(interaction_id: str, response: Any):
114
+ """
115
+ Alternative endpoint to respond by interaction ID only.
116
+
117
+ Useful when task_id is not readily available.
118
+ """
119
+ session = session_manager.find_session_by_interaction(interaction_id)
120
+ if not session:
121
+ raise HTTPException(
122
+ status_code=404, detail=f"Interaction {interaction_id} not found"
123
+ )
124
+
125
+ resolved = session.adapter.resolve_interaction(interaction_id, response)
126
+ if not resolved:
127
+ raise HTTPException(
128
+ status_code=400,
129
+ detail=f"Interaction {interaction_id} already resolved or not found",
130
+ )
131
+
132
+ return {
133
+ "status": "ok",
134
+ "interaction_id": interaction_id,
135
+ "task_id": session.current_task_id,
136
+ }
@@ -0,0 +1,135 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ Sessions API endpoints.
5
+
6
+ Handles session creation, retrieval, and management.
7
+ """
8
+
9
+ from fastapi import APIRouter, HTTPException
10
+ from pydantic import BaseModel, Field
11
+ from typing import Optional, List, Dict, Any
12
+
13
+ from ..services.session_manager import session_manager, HistoryMode
14
+
15
+ router = APIRouter(prefix="/api/sessions", tags=["sessions"])
16
+
17
+
18
+ class CreateSessionRequest(BaseModel):
19
+ """Request body for creating a session."""
20
+
21
+ project_path: str = Field(
22
+ default=".", description="Working directory for the session"
23
+ )
24
+ history_mode: Optional[HistoryMode] = Field(
25
+ default=None,
26
+ description="History mode: 'full' (stateless) or 'incremental' (stateful)",
27
+ )
28
+
29
+
30
+ class SessionResponse(BaseModel):
31
+ """Response for session operations."""
32
+
33
+ session_id: str
34
+ project_path: str
35
+ history_mode: str
36
+ created_at: float
37
+ message_count: int = 0
38
+
39
+
40
+ class SessionListResponse(BaseModel):
41
+ """Response for listing sessions."""
42
+
43
+ sessions: List[Dict[str, Any]]
44
+
45
+
46
+ @router.post("", response_model=SessionResponse)
47
+ async def create_session(request: CreateSessionRequest):
48
+ """
49
+ Create a new session.
50
+
51
+ Returns a session ID that can be used for subsequent chat requests.
52
+ """
53
+ try:
54
+ session = await session_manager.create_session(
55
+ project_path=request.project_path, history_mode=request.history_mode
56
+ )
57
+
58
+ return SessionResponse(
59
+ session_id=session.session_id,
60
+ project_path=session.project_path,
61
+ history_mode=session.history_mode,
62
+ created_at=session.created_at,
63
+ message_count=0,
64
+ )
65
+ except Exception as e:
66
+ raise HTTPException(status_code=500, detail=str(e))
67
+
68
+
69
+ @router.get("", response_model=SessionListResponse)
70
+ async def list_sessions():
71
+ """List all active sessions."""
72
+ sessions = session_manager.list_sessions()
73
+ return SessionListResponse(sessions=sessions)
74
+
75
+
76
+ @router.get("/{session_id}", response_model=SessionResponse)
77
+ async def get_session(session_id: str):
78
+ """
79
+ Get session details.
80
+
81
+ Returns session info including message history.
82
+ """
83
+ session = await session_manager.get_session(session_id)
84
+ if not session:
85
+ raise HTTPException(status_code=404, detail="Session not found")
86
+
87
+ messages = session_manager.get_messages(session)
88
+
89
+ return SessionResponse(
90
+ session_id=session.session_id,
91
+ project_path=session.project_path,
92
+ history_mode=session.history_mode,
93
+ created_at=session.created_at,
94
+ message_count=len(messages),
95
+ )
96
+
97
+
98
+ @router.get("/{session_id}/messages")
99
+ async def get_session_messages(session_id: str):
100
+ """
101
+ Get message history for a session.
102
+
103
+ Returns all messages in the conversation.
104
+ """
105
+ session = await session_manager.get_session(session_id)
106
+ if not session:
107
+ raise HTTPException(status_code=404, detail="Session not found")
108
+
109
+ messages = session_manager.get_messages(session)
110
+
111
+ return {"session_id": session_id, "messages": messages}
112
+
113
+
114
+ @router.delete("/{session_id}")
115
+ async def delete_session(session_id: str):
116
+ """Delete a session."""
117
+ deleted = await session_manager.delete_session(session_id)
118
+ if not deleted:
119
+ raise HTTPException(status_code=404, detail="Session not found")
120
+
121
+ return {"status": "ok", "session_id": session_id}
122
+
123
+
124
+ @router.post("/{session_id}/abort")
125
+ async def abort_session_task(session_id: str):
126
+ """
127
+ Abort the current task in a session.
128
+
129
+ Sends abort signal to stop ongoing processing.
130
+ """
131
+ aborted = await session_manager.abort_task(session_id)
132
+ if not aborted:
133
+ raise HTTPException(status_code=404, detail="Session not found")
134
+
135
+ return {"status": "ok", "session_id": session_id}
@@ -0,0 +1,149 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ FastAPI Web Server for minion-code.
5
+
6
+ Provides HTTP/SSE API for cross-process frontend communication.
7
+ """
8
+
9
+ import logging
10
+ from typing import Optional
11
+
12
+ from fastapi import FastAPI
13
+ from fastapi.middleware.cors import CORSMiddleware
14
+
15
+ from .api import chat_router, sessions_router, interactions_router
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+
20
+ def create_app(
21
+ title: str = "Minion Code API",
22
+ version: str = "1.0.0",
23
+ cors_origins: Optional[list] = None,
24
+ ) -> FastAPI:
25
+ """
26
+ Create and configure FastAPI application.
27
+
28
+ Args:
29
+ title: API title
30
+ version: API version
31
+ cors_origins: Allowed CORS origins (default: localhost:3000, localhost:5173)
32
+
33
+ Returns:
34
+ Configured FastAPI application
35
+ """
36
+ app = FastAPI(
37
+ title=title,
38
+ version=version,
39
+ description="""
40
+ Minion Code Web API
41
+
42
+ Provides streaming chat interface with:
43
+ - SSE (Server-Sent Events) for real-time responses
44
+ - A2A-style input_required for bidirectional interactions
45
+ - Session management with full/incremental history modes
46
+
47
+ ## Endpoints
48
+
49
+ ### Sessions
50
+ - `POST /api/sessions` - Create new session
51
+ - `GET /api/sessions` - List active sessions
52
+ - `GET /api/sessions/{id}` - Get session details
53
+ - `GET /api/sessions/{id}/messages` - Get message history
54
+ - `DELETE /api/sessions/{id}` - Delete session
55
+ - `POST /api/sessions/{id}/abort` - Abort current task
56
+
57
+ ### Chat
58
+ - `POST /api/chat/{session_id}` - Send message, receive SSE stream
59
+
60
+ ### Interactions
61
+ - `POST /api/tasks/{task_id}/input` - Respond to input_required event
62
+ - `POST /api/tasks/{task_id}/cancel` - Cancel pending interaction
63
+
64
+ ## SSE Event Types
65
+
66
+ - `task_status` - Task state changes
67
+ - `content` - Streaming text content
68
+ - `thinking` - LLM reasoning content
69
+ - `tool_call` - Tool invocation
70
+ - `tool_result` - Tool execution result
71
+ - `input_required` - Request for user input
72
+ - `error` - Error message
73
+
74
+ ## History Modes
75
+
76
+ - `full` - Each request creates new agent, loads full history (stateless, scalable)
77
+ - `incremental` - Reuse agent, only send new message (stateful, low latency)
78
+ """,
79
+ docs_url="/docs",
80
+ redoc_url="/redoc",
81
+ )
82
+
83
+ # CORS configuration
84
+ if cors_origins is None:
85
+ cors_origins = [
86
+ "http://localhost:3000",
87
+ "http://localhost:5173",
88
+ "http://127.0.0.1:3000",
89
+ "http://127.0.0.1:5173",
90
+ ]
91
+
92
+ app.add_middleware(
93
+ CORSMiddleware,
94
+ allow_origins=cors_origins,
95
+ allow_credentials=True,
96
+ allow_methods=["*"],
97
+ allow_headers=["*"],
98
+ )
99
+
100
+ # Register routers
101
+ app.include_router(sessions_router)
102
+ app.include_router(chat_router)
103
+ app.include_router(interactions_router)
104
+
105
+ # Health check endpoint
106
+ @app.get("/health")
107
+ async def health_check():
108
+ return {"status": "ok"}
109
+
110
+ # Root endpoint
111
+ @app.get("/")
112
+ async def root():
113
+ return {"name": title, "version": version, "docs": "/docs", "health": "/health"}
114
+
115
+ return app
116
+
117
+
118
+ def run_server(
119
+ host: str = "0.0.0.0",
120
+ port: int = 8000,
121
+ reload: bool = False,
122
+ log_level: str = "info",
123
+ ):
124
+ """
125
+ Run the web server.
126
+
127
+ Args:
128
+ host: Host to bind to
129
+ port: Port to listen on
130
+ reload: Enable auto-reload for development
131
+ log_level: Logging level
132
+ """
133
+ import uvicorn
134
+
135
+ logger.info(f"Starting Minion Code Web API on {host}:{port}")
136
+
137
+ uvicorn.run(
138
+ "minion_code.web.server:create_app",
139
+ factory=True,
140
+ host=host,
141
+ port=port,
142
+ reload=reload,
143
+ log_level=log_level,
144
+ )
145
+
146
+
147
+ # For direct module execution
148
+ if __name__ == "__main__":
149
+ run_server()
@@ -0,0 +1,5 @@
1
+ """Web services."""
2
+
3
+ from .session_manager import SessionManager, session_manager
4
+
5
+ __all__ = ["SessionManager", "session_manager"]