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,524 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ Web Output Adapter - SSE implementation for cross-process communication.
5
+
6
+ This adapter outputs events to an asyncio.Queue for SSE streaming.
7
+ It follows the A2A-style input_required pattern for bidirectional interactions.
8
+ """
9
+
10
+ from typing import List, Optional, Dict, Any, Literal
11
+ import asyncio
12
+ import time
13
+ from dataclasses import dataclass, field, asdict
14
+ from enum import Enum
15
+
16
+ from minion_code.adapters.output_adapter import OutputAdapter
17
+
18
+
19
+ class TaskState(str, Enum):
20
+ """Task lifecycle states (A2A style)"""
21
+
22
+ SUBMITTED = "submitted"
23
+ WORKING = "working"
24
+ INPUT_REQUIRED = "input_required"
25
+ COMPLETED = "completed"
26
+ FAILED = "failed"
27
+ CANCELLED = "cancelled"
28
+
29
+
30
+ class InputKind(str, Enum):
31
+ """Types of user input requests"""
32
+
33
+ PERMISSION = "permission"
34
+ TEXT = "text"
35
+ CHOICE = "choice"
36
+ FORM = "form"
37
+
38
+
39
+ @dataclass
40
+ class PermissionData:
41
+ """Data for permission requests (tool use confirm)"""
42
+
43
+ resource_type: str # "tool", "file", "network", "system"
44
+ resource_name: str
45
+ resource_args: Optional[Dict[str, Any]] = None
46
+ risk_level: str = "medium" # "low", "medium", "high"
47
+
48
+
49
+ @dataclass
50
+ class TextInputData:
51
+ """Data for text input requests"""
52
+
53
+ placeholder: str = ""
54
+ default_value: str = ""
55
+ multiline: bool = False
56
+ max_length: Optional[int] = None
57
+
58
+
59
+ @dataclass
60
+ class ChoiceData:
61
+ """Data for choice requests"""
62
+
63
+ choices: List[Dict[str, Any]] = field(default_factory=list)
64
+ allow_multiple: bool = False
65
+ default_index: int = 0
66
+
67
+
68
+ @dataclass
69
+ class InputRequest:
70
+ """Request for user input (A2A input_required)"""
71
+
72
+ interaction_id: str
73
+ kind: str # InputKind value
74
+ title: str
75
+ message: str
76
+ data: Dict[str, Any]
77
+ timeout_seconds: Optional[int] = 300
78
+
79
+
80
+ @dataclass
81
+ class SSEEvent:
82
+ """Server-Sent Event data structure"""
83
+
84
+ type: str
85
+ data: Dict[str, Any]
86
+ task_id: Optional[str] = None
87
+ timestamp: float = field(default_factory=time.time)
88
+
89
+ def to_dict(self) -> Dict[str, Any]:
90
+ result = {"type": self.type, "timestamp": self.timestamp, **self.data}
91
+ if self.task_id:
92
+ result["task_id"] = self.task_id
93
+ return result
94
+
95
+
96
+ @dataclass
97
+ class PendingInteraction:
98
+ """Represents a user interaction waiting for response"""
99
+
100
+ interaction_id: str
101
+ kind: str
102
+ data: Dict[str, Any]
103
+ future: asyncio.Future
104
+
105
+
106
+ class WebOutputAdapter(OutputAdapter):
107
+ """
108
+ Web SSE adapter for cross-process frontend communication.
109
+
110
+ This adapter:
111
+ 1. Outputs events to an asyncio.Queue for SSE streaming
112
+ 2. Uses asyncio.Future for blocking interactions (confirm, choice, input)
113
+ 3. Follows A2A-style input_required pattern
114
+
115
+ Workflow:
116
+ 1. Agent calls adapter methods (panel, text, confirm, etc.)
117
+ 2. Adapter creates SSEEvent and puts to event_queue
118
+ 3. API endpoint reads from event_queue and sends SSE to client
119
+ 4. For interactions, adapter creates Future and waits
120
+ 5. Client responds via HTTP POST → resolve_interaction() → Future completes
121
+ """
122
+
123
+ def __init__(
124
+ self, session_id: str, task_id: Optional[str] = None, timeout_seconds: int = 300
125
+ ):
126
+ """
127
+ Initialize Web adapter.
128
+
129
+ Args:
130
+ session_id: Session identifier
131
+ task_id: Current task identifier (set per query)
132
+ timeout_seconds: Default timeout for interactions
133
+ """
134
+ self.session_id = session_id
135
+ self.task_id = task_id
136
+ self.timeout_seconds = timeout_seconds
137
+
138
+ # Event queue for SSE output
139
+ self.event_queue: asyncio.Queue[SSEEvent] = asyncio.Queue()
140
+
141
+ # Pending interactions waiting for user response
142
+ self._pending_interactions: Dict[str, PendingInteraction] = {}
143
+ self._interaction_counter = 0
144
+ self._message_counter = 0
145
+
146
+ # Current task state
147
+ self._task_state = TaskState.SUBMITTED
148
+
149
+ def set_task_id(self, task_id: str):
150
+ """Set current task ID for new query."""
151
+ self.task_id = task_id
152
+ self._task_state = TaskState.SUBMITTED
153
+
154
+ def _generate_interaction_id(self) -> str:
155
+ """Generate unique interaction ID."""
156
+ self._interaction_counter += 1
157
+ return f"int_{self.session_id}_{self._interaction_counter}"
158
+
159
+ def _generate_message_id(self) -> str:
160
+ """Generate unique message ID."""
161
+ self._message_counter += 1
162
+ return f"msg_{self.session_id}_{self._message_counter}"
163
+
164
+ async def _emit_event(self, event_type: str, data: Dict[str, Any]):
165
+ """Emit SSE event to queue."""
166
+ event = SSEEvent(type=event_type, data=data, task_id=self.task_id)
167
+ await self.event_queue.put(event)
168
+
169
+ def _emit_event_sync(self, event_type: str, data: Dict[str, Any]):
170
+ """Emit SSE event synchronously (for non-async methods)."""
171
+ event = SSEEvent(type=event_type, data=data, task_id=self.task_id)
172
+ # Use put_nowait for sync context
173
+ try:
174
+ self.event_queue.put_nowait(event)
175
+ except asyncio.QueueFull:
176
+ pass # Drop event if queue is full
177
+
178
+ async def emit_task_status(self, state: TaskState):
179
+ """Emit task status change event."""
180
+ self._task_state = state
181
+ await self._emit_event(
182
+ "task_status", {"state": state.value, "session_id": self.session_id}
183
+ )
184
+
185
+ # ========== OutputAdapter interface implementation ==========
186
+
187
+ def panel(self, content: str, title: str = "", border_style: str = "blue") -> None:
188
+ """Send panel output as SSE event."""
189
+ self._emit_event_sync(
190
+ "panel", {"content": content, "title": title, "style": border_style}
191
+ )
192
+
193
+ def table(self, headers: List[str], rows: List[List[str]], title: str = "") -> None:
194
+ """Send table output as SSE event."""
195
+ self._emit_event_sync(
196
+ "table", {"headers": headers, "rows": rows, "title": title}
197
+ )
198
+
199
+ def text(self, content: str, style: str = "") -> None:
200
+ """Send text output as SSE event."""
201
+ self._emit_event_sync("text", {"content": content, "style": style})
202
+
203
+ async def confirm(
204
+ self,
205
+ message: str,
206
+ title: str = "Confirm",
207
+ default: bool = False,
208
+ ok_text: str = "Yes",
209
+ cancel_text: str = "No",
210
+ ) -> bool:
211
+ """
212
+ Request user confirmation via input_required event.
213
+
214
+ This method:
215
+ 1. Changes task state to input_required
216
+ 2. Sends input_required SSE event
217
+ 3. Creates Future and waits for response
218
+ 4. Changes task state back to working
219
+ """
220
+ interaction_id = self._generate_interaction_id()
221
+ future: asyncio.Future[bool] = asyncio.Future()
222
+
223
+ # Store pending interaction
224
+ self._pending_interactions[interaction_id] = PendingInteraction(
225
+ interaction_id=interaction_id,
226
+ kind=InputKind.PERMISSION.value,
227
+ data={
228
+ "message": message,
229
+ "title": title,
230
+ "default": default,
231
+ "ok_text": ok_text,
232
+ "cancel_text": cancel_text,
233
+ },
234
+ future=future,
235
+ )
236
+
237
+ # Change state to input_required
238
+ await self.emit_task_status(TaskState.INPUT_REQUIRED)
239
+
240
+ # Send input_required event
241
+ await self._emit_event(
242
+ "input_required",
243
+ {
244
+ "request": {
245
+ "interaction_id": interaction_id,
246
+ "kind": InputKind.PERMISSION.value,
247
+ "title": title,
248
+ "message": message,
249
+ "data": {
250
+ "resource_type": "action",
251
+ "resource_name": title,
252
+ "default": default,
253
+ "ok_text": ok_text,
254
+ "cancel_text": cancel_text,
255
+ },
256
+ "timeout_seconds": self.timeout_seconds,
257
+ }
258
+ },
259
+ )
260
+
261
+ # Wait for user response
262
+ try:
263
+ result = await asyncio.wait_for(future, timeout=self.timeout_seconds)
264
+ return result
265
+ except asyncio.TimeoutError:
266
+ return default
267
+ finally:
268
+ self._pending_interactions.pop(interaction_id, None)
269
+ # Change state back to working
270
+ await self.emit_task_status(TaskState.WORKING)
271
+
272
+ async def choice(
273
+ self,
274
+ message: str,
275
+ choices: List[str],
276
+ title: str = "Select",
277
+ default_index: int = 0,
278
+ ) -> int:
279
+ """
280
+ Request user choice selection via input_required event.
281
+
282
+ Returns the selected index (0-based), or -1 if cancelled/timeout.
283
+ """
284
+ interaction_id = self._generate_interaction_id()
285
+ future: asyncio.Future[int] = asyncio.Future()
286
+
287
+ self._pending_interactions[interaction_id] = PendingInteraction(
288
+ interaction_id=interaction_id,
289
+ kind=InputKind.CHOICE.value,
290
+ data={
291
+ "message": message,
292
+ "choices": choices,
293
+ "title": title,
294
+ "default_index": default_index,
295
+ },
296
+ future=future,
297
+ )
298
+
299
+ await self.emit_task_status(TaskState.INPUT_REQUIRED)
300
+
301
+ await self._emit_event(
302
+ "input_required",
303
+ {
304
+ "request": {
305
+ "interaction_id": interaction_id,
306
+ "kind": InputKind.CHOICE.value,
307
+ "title": title,
308
+ "message": message,
309
+ "data": {
310
+ "choices": [
311
+ {"label": c, "value": i} for i, c in enumerate(choices)
312
+ ],
313
+ "default_index": default_index,
314
+ },
315
+ "timeout_seconds": self.timeout_seconds,
316
+ }
317
+ },
318
+ )
319
+
320
+ try:
321
+ result = await asyncio.wait_for(future, timeout=self.timeout_seconds)
322
+ return result
323
+ except asyncio.TimeoutError:
324
+ return -1
325
+ finally:
326
+ self._pending_interactions.pop(interaction_id, None)
327
+ await self.emit_task_status(TaskState.WORKING)
328
+
329
+ async def input(
330
+ self,
331
+ message: str,
332
+ title: str = "Input",
333
+ default: str = "",
334
+ placeholder: str = "",
335
+ ) -> Optional[str]:
336
+ """
337
+ Request user text input via input_required event.
338
+
339
+ Returns the input string, or None if cancelled/timeout.
340
+ """
341
+ interaction_id = self._generate_interaction_id()
342
+ future: asyncio.Future[Optional[str]] = asyncio.Future()
343
+
344
+ self._pending_interactions[interaction_id] = PendingInteraction(
345
+ interaction_id=interaction_id,
346
+ kind=InputKind.TEXT.value,
347
+ data={
348
+ "message": message,
349
+ "title": title,
350
+ "default": default,
351
+ "placeholder": placeholder,
352
+ },
353
+ future=future,
354
+ )
355
+
356
+ await self.emit_task_status(TaskState.INPUT_REQUIRED)
357
+
358
+ await self._emit_event(
359
+ "input_required",
360
+ {
361
+ "request": {
362
+ "interaction_id": interaction_id,
363
+ "kind": InputKind.TEXT.value,
364
+ "title": title,
365
+ "message": message,
366
+ "data": {"placeholder": placeholder, "default_value": default},
367
+ "timeout_seconds": self.timeout_seconds,
368
+ }
369
+ },
370
+ )
371
+
372
+ try:
373
+ result = await asyncio.wait_for(future, timeout=self.timeout_seconds)
374
+ return result
375
+ except asyncio.TimeoutError:
376
+ return None
377
+ finally:
378
+ self._pending_interactions.pop(interaction_id, None)
379
+ await self.emit_task_status(TaskState.WORKING)
380
+
381
+ def print(self, *args, **kwargs) -> None:
382
+ """Generic print - converts to text output."""
383
+ content = " ".join(str(arg) for arg in args)
384
+ self.text(content)
385
+
386
+ # ========== Interaction resolution ==========
387
+
388
+ def resolve_interaction(self, interaction_id: str, result: Any) -> bool:
389
+ """
390
+ Resolve a pending interaction with user's response.
391
+
392
+ This method should be called by the HTTP endpoint when
393
+ user responds to an input_required event.
394
+
395
+ Args:
396
+ interaction_id: The interaction ID from the request
397
+ result: The user's response (bool, int, or str depending on kind)
398
+
399
+ Returns:
400
+ True if interaction was found and resolved, False otherwise
401
+ """
402
+ interaction = self._pending_interactions.get(interaction_id)
403
+ if interaction and not interaction.future.done():
404
+ interaction.future.set_result(result)
405
+ return True
406
+ return False
407
+
408
+ def cancel_interaction(self, interaction_id: str) -> bool:
409
+ """
410
+ Cancel a pending interaction.
411
+
412
+ Sets appropriate default value based on interaction kind.
413
+ """
414
+ interaction = self._pending_interactions.get(interaction_id)
415
+ if interaction and not interaction.future.done():
416
+ if interaction.kind == InputKind.PERMISSION.value:
417
+ interaction.future.set_result(False)
418
+ elif interaction.kind == InputKind.CHOICE.value:
419
+ interaction.future.set_result(-1)
420
+ elif interaction.kind == InputKind.TEXT.value:
421
+ interaction.future.set_result(None)
422
+ return True
423
+ return False
424
+
425
+ def get_pending_interaction(
426
+ self, interaction_id: str
427
+ ) -> Optional[PendingInteraction]:
428
+ """Get a pending interaction by ID."""
429
+ return self._pending_interactions.get(interaction_id)
430
+
431
+ def has_pending_interactions(self) -> bool:
432
+ """Check if there are any pending interactions."""
433
+ return len(self._pending_interactions) > 0
434
+
435
+ # ========== Streaming helpers ==========
436
+
437
+ async def emit_content(self, chunk: str):
438
+ """Emit streaming content chunk."""
439
+ await self._emit_event("content", {"chunk": chunk})
440
+
441
+ async def emit_thinking(self, chunk: str):
442
+ """Emit thinking/reasoning content chunk."""
443
+ await self._emit_event("thinking", {"chunk": chunk})
444
+
445
+ async def emit_tool_call(self, name: str, args: Dict[str, Any]):
446
+ """Emit tool call event."""
447
+ await self._emit_event("tool_call", {"name": name, "args": args})
448
+
449
+ async def emit_tool_result(self, success: bool, output: str):
450
+ """Emit tool result event."""
451
+ await self._emit_event("tool_result", {"success": success, "output": output})
452
+
453
+ async def emit_error(self, message: str, code: Optional[str] = None):
454
+ """Emit error event."""
455
+ data = {"message": message}
456
+ if code:
457
+ data["code"] = code
458
+ await self._emit_event("error", data)
459
+
460
+ async def emit_done(self):
461
+ """Emit stream end event."""
462
+ await self._emit_event("done", {})
463
+
464
+ # ========== Tool permission helper ==========
465
+
466
+ async def confirm_tool_use(
467
+ self, tool_name: str, tool_args: Dict[str, Any], description: str = ""
468
+ ) -> bool:
469
+ """
470
+ Request user permission for tool execution.
471
+
472
+ This is a convenience method for tool permission confirmation.
473
+
474
+ Args:
475
+ tool_name: Name of the tool to execute
476
+ tool_args: Arguments to the tool
477
+ description: Human-readable description of what the tool will do
478
+
479
+ Returns:
480
+ True if user allows, False if denied or timeout
481
+ """
482
+ interaction_id = self._generate_interaction_id()
483
+ future: asyncio.Future[bool] = asyncio.Future()
484
+
485
+ self._pending_interactions[interaction_id] = PendingInteraction(
486
+ interaction_id=interaction_id,
487
+ kind=InputKind.PERMISSION.value,
488
+ data={
489
+ "tool_name": tool_name,
490
+ "tool_args": tool_args,
491
+ "description": description,
492
+ },
493
+ future=future,
494
+ )
495
+
496
+ await self.emit_task_status(TaskState.INPUT_REQUIRED)
497
+
498
+ await self._emit_event(
499
+ "input_required",
500
+ {
501
+ "request": {
502
+ "interaction_id": interaction_id,
503
+ "kind": InputKind.PERMISSION.value,
504
+ "title": f"Allow {tool_name}?",
505
+ "message": description or f"Agent wants to execute {tool_name}",
506
+ "data": {
507
+ "resource_type": "tool",
508
+ "resource_name": tool_name,
509
+ "resource_args": tool_args,
510
+ "risk_level": "medium",
511
+ },
512
+ "timeout_seconds": self.timeout_seconds,
513
+ }
514
+ },
515
+ )
516
+
517
+ try:
518
+ result = await asyncio.wait_for(future, timeout=self.timeout_seconds)
519
+ return result
520
+ except asyncio.TimeoutError:
521
+ return False
522
+ finally:
523
+ self._pending_interactions.pop(interaction_id, None)
524
+ await self.emit_task_status(TaskState.WORKING)
@@ -0,0 +1,7 @@
1
+ """API routes for web server."""
2
+
3
+ from .chat import router as chat_router
4
+ from .sessions import router as sessions_router
5
+ from .interactions import router as interactions_router
6
+
7
+ __all__ = ["chat_router", "sessions_router", "interactions_router"]