agentx-dev 3.0.2__tar.gz → 3.0.3__tar.gz

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 (55) hide show
  1. {agentx_dev-3.0.2/agentx_dev.egg-info → agentx_dev-3.0.3}/PKG-INFO +1 -1
  2. {agentx_dev-3.0.2 → agentx_dev-3.0.3}/agentx_dev/Agents/Agent.py +139 -14
  3. {agentx_dev-3.0.2 → agentx_dev-3.0.3}/agentx_dev/Runner/AgentRun.py +57 -19
  4. {agentx_dev-3.0.2 → agentx_dev-3.0.3}/agentx_dev/Runner/AsyncAgentRun.py +31 -2
  5. {agentx_dev-3.0.2 → agentx_dev-3.0.3/agentx_dev.egg-info}/PKG-INFO +1 -1
  6. {agentx_dev-3.0.2 → agentx_dev-3.0.3}/agentx_dev.egg-info/top_level.txt +0 -1
  7. {agentx_dev-3.0.2 → agentx_dev-3.0.3}/pyproject.toml +1 -1
  8. {agentx_dev-3.0.2 → agentx_dev-3.0.3}/AGENTX.md +0 -0
  9. {agentx_dev-3.0.2 → agentx_dev-3.0.3}/LICENSE +0 -0
  10. {agentx_dev-3.0.2 → agentx_dev-3.0.3}/MANIFEST.in +0 -0
  11. {agentx_dev-3.0.2 → agentx_dev-3.0.3}/README.md +0 -0
  12. {agentx_dev-3.0.2 → agentx_dev-3.0.3}/agentx_dev/Agents/__init__.py +0 -0
  13. {agentx_dev-3.0.2 → agentx_dev-3.0.3}/agentx_dev/AsyncTools.py +0 -0
  14. {agentx_dev-3.0.2 → agentx_dev-3.0.3}/agentx_dev/AutoSetup.py +0 -0
  15. {agentx_dev-3.0.2 → agentx_dev-3.0.3}/agentx_dev/Cache.py +0 -0
  16. {agentx_dev-3.0.2 → agentx_dev-3.0.3}/agentx_dev/ChatModel.py +0 -0
  17. {agentx_dev-3.0.2 → agentx_dev-3.0.3}/agentx_dev/Config.py +0 -0
  18. {agentx_dev-3.0.2 → agentx_dev-3.0.3}/agentx_dev/DefaultTools.py +0 -0
  19. {agentx_dev-3.0.2 → agentx_dev-3.0.3}/agentx_dev/Loader.py +0 -0
  20. {agentx_dev-3.0.2 → agentx_dev-3.0.3}/agentx_dev/MCP.py +0 -0
  21. {agentx_dev-3.0.2 → agentx_dev-3.0.3}/agentx_dev/Memory.py +0 -0
  22. {agentx_dev-3.0.2 → agentx_dev-3.0.3}/agentx_dev/Observability.py +0 -0
  23. {agentx_dev-3.0.2 → agentx_dev-3.0.3}/agentx_dev/Planner.py +0 -0
  24. {agentx_dev-3.0.2 → agentx_dev-3.0.3}/agentx_dev/Runner/__init__.py +0 -0
  25. {agentx_dev-3.0.2 → agentx_dev-3.0.3}/agentx_dev/Runner/promptTemplate.yaml +0 -0
  26. {agentx_dev-3.0.2 → agentx_dev-3.0.3}/agentx_dev/Session.py +0 -0
  27. {agentx_dev-3.0.2 → agentx_dev-3.0.3}/agentx_dev/Streaming.py +0 -0
  28. {agentx_dev-3.0.2 → agentx_dev-3.0.3}/agentx_dev/Supervisor.py +0 -0
  29. {agentx_dev-3.0.2 → agentx_dev-3.0.3}/agentx_dev/Tools.py +0 -0
  30. {agentx_dev-3.0.2 → agentx_dev-3.0.3}/agentx_dev/WebTools.py +0 -0
  31. {agentx_dev-3.0.2 → agentx_dev-3.0.3}/agentx_dev/__init__.py +0 -0
  32. {agentx_dev-3.0.2 → agentx_dev-3.0.3}/agentx_dev/resources/__init__.py +0 -0
  33. {agentx_dev-3.0.2 → agentx_dev-3.0.3}/agentx_dev/resources/promptTemplate.yaml +0 -0
  34. {agentx_dev-3.0.2 → agentx_dev-3.0.3}/agentx_dev.egg-info/SOURCES.txt +0 -0
  35. {agentx_dev-3.0.2 → agentx_dev-3.0.3}/agentx_dev.egg-info/dependency_links.txt +0 -0
  36. {agentx_dev-3.0.2 → agentx_dev-3.0.3}/agentx_dev.egg-info/requires.txt +0 -0
  37. {agentx_dev-3.0.2 → agentx_dev-3.0.3}/examples/README.md +0 -0
  38. {agentx_dev-3.0.2 → agentx_dev-3.0.3}/examples/async_example.py +0 -0
  39. {agentx_dev-3.0.2 → agentx_dev-3.0.3}/examples/async_quickstart.py +0 -0
  40. {agentx_dev-3.0.2 → agentx_dev-3.0.3}/examples/auto_features_example.py +0 -0
  41. {agentx_dev-3.0.2 → agentx_dev-3.0.3}/examples/caching_example.py +0 -0
  42. {agentx_dev-3.0.2 → agentx_dev-3.0.3}/examples/chatbot_example.py +0 -0
  43. {agentx_dev-3.0.2 → agentx_dev-3.0.3}/examples/complete_example.py +0 -0
  44. {agentx_dev-3.0.2 → agentx_dev-3.0.3}/examples/concurrent_example.py +0 -0
  45. {agentx_dev-3.0.2 → agentx_dev-3.0.3}/examples/concurrent_tool_example.py +0 -0
  46. {agentx_dev-3.0.2 → agentx_dev-3.0.3}/examples/file_agent_demo.py +0 -0
  47. {agentx_dev-3.0.2 → agentx_dev-3.0.3}/examples/function_calling_demo.py +0 -0
  48. {agentx_dev-3.0.2 → agentx_dev-3.0.3}/examples/mcp_demo.py +0 -0
  49. {agentx_dev-3.0.2 → agentx_dev-3.0.3}/examples/observability_example.py +0 -0
  50. {agentx_dev-3.0.2 → agentx_dev-3.0.3}/examples/orchestration_demo.py +0 -0
  51. {agentx_dev-3.0.2 → agentx_dev-3.0.3}/examples/planner_example.py +0 -0
  52. {agentx_dev-3.0.2 → agentx_dev-3.0.3}/examples/supervisor_codebase_analysis_demo.py +0 -0
  53. {agentx_dev-3.0.2 → agentx_dev-3.0.3}/examples/supervisor_example.py +0 -0
  54. {agentx_dev-3.0.2 → agentx_dev-3.0.3}/examples/sync_quickstart.py +0 -0
  55. {agentx_dev-3.0.2 → agentx_dev-3.0.3}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentx-dev
3
- Version: 3.0.2
3
+ Version: 3.0.3
4
4
  Summary: A lightweight LLM agent framework with multi-agent orchestration (Supervisor + dynamic specialist spawning), permission-gated default tools, MCP integration, and prompt-template presets.
5
5
  Author-email: Bruce-Arhin Shadrach <brucearhin098@gmail.com>
6
6
  License: MIT License
@@ -99,8 +99,28 @@ class StandardParser(_ParserMixin, BaseModel):
99
99
  - from_json: Creates an instance from a JSON string.
100
100
  - from_function_call: Creates an instance from a tool-call args dict.
101
101
  """
102
- action: str = Field("The action to take", alias='action')
103
- action_input: str | Dict | List = Field("The input to the action", alias='action_input')
102
+ action: str = Field(
103
+ default="",
104
+ description=(
105
+ "REQUIRED. Must be one of the registered tool names shown "
106
+ "in the system prompt, OR the exact literal string "
107
+ "'Final_Answer' (with underscore) when you are ready to "
108
+ "return the completed answer. Do NOT put natural-language "
109
+ "response text here — that goes in action_input."
110
+ ),
111
+ alias='action',
112
+ )
113
+ action_input: str | Dict | List = Field(
114
+ default="",
115
+ description=(
116
+ "REQUIRED. When action is a tool name, this is the JSON "
117
+ "object matching that tool's argument schema (or the "
118
+ "single string the tool expects). When action is "
119
+ "'Final_Answer', this is the actual answer TEXT you want "
120
+ "the user to see."
121
+ ),
122
+ alias='action_input',
123
+ )
104
124
 
105
125
  model_config = ConfigDict(populate_by_name=True)
106
126
 
@@ -150,8 +170,27 @@ class Instruction_Tuned_(_ParserMixin, BaseModel):
150
170
  - action: str, the action to take or final answer.
151
171
  - action_input: str, Dict, or List, the input to the action.
152
172
  """
153
- action: str = Field("The action to take OR Final Answer", alias="action")
154
- action_input: str | Dict | List = Field("The input to the action OR You should put what you want to return to use here and return to user immediately", alias="action_input")
173
+ action: str = Field(
174
+ default="",
175
+ description=(
176
+ "REQUIRED. Must be one of the registered tool names shown "
177
+ "in the system prompt, OR the exact literal string "
178
+ "'Final_Answer' (with underscore) when you are ready to "
179
+ "return the completed answer. Never put natural-language "
180
+ "response text here — that goes in action_input."
181
+ ),
182
+ alias='action',
183
+ )
184
+ action_input: str | Dict | List = Field(
185
+ default="",
186
+ description=(
187
+ "REQUIRED. When action is a tool name, this is the JSON "
188
+ "object matching that tool's schema (or the single string "
189
+ "it expects). When action is 'Final_Answer', this is the "
190
+ "actual answer TEXT you want the user to see."
191
+ ),
192
+ alias='action_input',
193
+ )
155
194
 
156
195
  model_config = ConfigDict(populate_by_name=True)
157
196
 
@@ -172,9 +211,36 @@ class React_(_ParserMixin, BaseModel):
172
211
  - action: str, the action to take or final answer.
173
212
  - action_input: str, Dict, or List, the input to the action.
174
213
  """
175
- Thought: str = Field('The Agent Thoughts', alias='Thought')
176
- action: str = Field("The action to take OR Final Answer", alias="action")
177
- action_input: str | Dict | List = Field("The input to the action OR You should put what you want to return to use here and return to user immediately", alias="action_input")
214
+ Thought: str = Field(
215
+ default="",
216
+ description=(
217
+ "REQUIRED. Brief reasoning for THIS turn: what the last "
218
+ "tool returned + what you'll do differently now. Keep it "
219
+ "concrete, not generic."
220
+ ),
221
+ alias='Thought',
222
+ )
223
+ action: str = Field(
224
+ default="",
225
+ description=(
226
+ "REQUIRED. Must be one of the registered tool names shown "
227
+ "in the system prompt, OR the exact literal string "
228
+ "'Final_Answer' (with underscore) when you are ready to "
229
+ "return the completed answer. Never put natural-language "
230
+ "response text here — that goes in action_input."
231
+ ),
232
+ alias='action',
233
+ )
234
+ action_input: str | Dict | List = Field(
235
+ default="",
236
+ description=(
237
+ "REQUIRED. When action is a tool name, this is the JSON "
238
+ "object matching that tool's schema (or the single string "
239
+ "it expects). When action is 'Final_Answer', this is the "
240
+ "actual answer TEXT you want the user to see."
241
+ ),
242
+ alias='action_input',
243
+ )
178
244
 
179
245
  model_config = ConfigDict(populate_by_name=True)
180
246
 
@@ -198,14 +264,35 @@ class ChainOfThought(_ParserMixin, BaseModel):
198
264
  ``instance.action`` / ``instance.action_input`` uniformly across every
199
265
  AgentType — no more parser-specific field lookups.
200
266
  """
201
- Thought: str = Field('The step-by-step reasoning', alias='Thought')
267
+ Thought: str = Field(
268
+ default="",
269
+ description=(
270
+ "REQUIRED. Step-by-step reasoning covering: (a) what the "
271
+ "user asked; (b) what info is needed; (c) which tool fits; "
272
+ "(d) the exact argument shape; (e) whether one call finishes."
273
+ ),
274
+ alias='Thought',
275
+ )
202
276
  action: str = Field(
203
- "The action to take or Final Answer",
277
+ default="",
278
+ description=(
279
+ "REQUIRED. Must be one of the registered tool names shown "
280
+ "in the system prompt, OR the exact literal string "
281
+ "'Final_Answer' (with underscore) when you are ready to "
282
+ "return the completed answer. Never put natural-language "
283
+ "response text here — that goes in action_input."
284
+ ),
204
285
  validation_alias=AliasChoices("action", "Action"),
205
286
  serialization_alias="action",
206
287
  )
207
288
  action_input: str | Dict | List = Field(
208
- "The input to the action",
289
+ default="",
290
+ description=(
291
+ "REQUIRED. When action is a tool name, this is the JSON "
292
+ "object matching that tool's schema (or the single string "
293
+ "it expects). When action is 'Final_Answer', this is the "
294
+ "actual answer TEXT you want the user to see."
295
+ ),
209
296
  validation_alias=AliasChoices("action_input", "Action_Input"),
210
297
  serialization_alias="action_input",
211
298
  )
@@ -224,8 +311,27 @@ class ZeroShot(_ParserMixin, BaseModel):
224
311
  """
225
312
  A Pydantic model representing the Zero-Shot prompt template.
226
313
  """
227
- action: str = Field("The action to take", alias='action')
228
- action_input: str | Dict | List = Field("The input to the action", alias='action_input')
314
+ action: str = Field(
315
+ default="",
316
+ description=(
317
+ "REQUIRED. Must be one of the registered tool names shown "
318
+ "in the system prompt, OR the exact literal string "
319
+ "'Final_Answer' (with underscore) when you are ready to "
320
+ "return the completed answer. Never put natural-language "
321
+ "response text here — that goes in action_input."
322
+ ),
323
+ alias='action',
324
+ )
325
+ action_input: str | Dict | List = Field(
326
+ default="",
327
+ description=(
328
+ "REQUIRED. When action is a tool name, this is the JSON "
329
+ "object matching that tool's schema (or the single string "
330
+ "it expects). When action is 'Final_Answer', this is the "
331
+ "actual answer TEXT you want the user to see."
332
+ ),
333
+ alias='action_input',
334
+ )
229
335
 
230
336
  model_config = ConfigDict(populate_by_name=True)
231
337
 
@@ -241,8 +347,27 @@ class FewShot(_ParserMixin, BaseModel):
241
347
  """
242
348
  A Pydantic model representing the Few-Shot prompt template.
243
349
  """
244
- action: str = Field("The action to take", alias='action')
245
- action_input: str | Dict | List = Field("The input to the action", alias='action_input')
350
+ action: str = Field(
351
+ default="",
352
+ description=(
353
+ "REQUIRED. Must be one of the registered tool names shown "
354
+ "in the system prompt, OR the exact literal string "
355
+ "'Final_Answer' (with underscore) when you are ready to "
356
+ "return the completed answer. Never put natural-language "
357
+ "response text here — that goes in action_input."
358
+ ),
359
+ alias='action',
360
+ )
361
+ action_input: str | Dict | List = Field(
362
+ default="",
363
+ description=(
364
+ "REQUIRED. When action is a tool name, this is the JSON "
365
+ "object matching that tool's schema (or the single string "
366
+ "it expects). When action is 'Final_Answer', this is the "
367
+ "actual answer TEXT you want the user to see."
368
+ ),
369
+ alias='action_input',
370
+ )
246
371
 
247
372
  model_config = ConfigDict(populate_by_name=True)
248
373
 
@@ -114,6 +114,34 @@ class CircuitBreaker:
114
114
  _NO_ARG = object()
115
115
 
116
116
 
117
+ # Set of case-insensitive strings that should be treated as the model's
118
+ # "I'm done, return this as the final answer" signal. Exact string
119
+ # "Final_Answer" is the canonical form the prompt template teaches;
120
+ # the tolerant variants exist because under use_function_calling=True
121
+ # the schema doesn't constrain `action` to any specific value, so
122
+ # LLMs (especially gpt-4o-mini) emit "Final Answer" (with space) or
123
+ # "final_answer" (lowercase) or "finalanswer" (no separator). Any of
124
+ # those should end the loop, not get routed to Tool_Runner.
125
+ #
126
+ # Deliberately excludes bare "answer" or "final" — those are too
127
+ # likely to collide with a user-defined tool of the same name.
128
+ _TERMINAL_ACTION_VARIANTS = frozenset({
129
+ "final_answer", "finalanswer",
130
+ })
131
+
132
+
133
+ def _is_terminal_action(action: str) -> bool:
134
+ """True if ``action`` should end the loop with action_input as the
135
+ final answer. Accepts Final_Answer, Final Answer, final_answer,
136
+ FINAL_ANSWER, finalanswer — any capitalization + underscore/space
137
+ variant. Empty string is not terminal (caller handles that as
138
+ 'unrecognized action' via the implicit-final guardrail)."""
139
+ if not action or not isinstance(action, str):
140
+ return False
141
+ normalized = action.strip().lower().replace(" ", "_").replace("-", "_")
142
+ return normalized in _TERMINAL_ACTION_VARIANTS
143
+
144
+
117
145
  def _build_sandbox_hint(perms: Any) -> Optional[str]:
118
146
  """Generate a short block describing where the runner is allowed to
119
147
  operate on disk. Injected into the system prompt when Permissions
@@ -1323,42 +1351,52 @@ class AgentRunner:
1323
1351
  step_description = f"Step {count}: {action} with {action_input}"
1324
1352
  steps.append(step_description)
1325
1353
 
1326
- if action == "Final_Answer":
1354
+ # Terminal action check — tolerant matching so the loop ends
1355
+ # for the canonical "Final_Answer" AND for the LLM's common
1356
+ # variants ("Final Answer", "final_answer", "finalanswer").
1357
+ # Under use_function_calling=True the schema doesn't
1358
+ # constrain `action` to any spelling, so we accept them all.
1359
+ if _is_terminal_action(action):
1327
1360
  final_answer = action_input
1328
1361
  yield {"type": "final", "content": final_answer}
1329
1362
  break
1330
1363
 
1331
- # Guardrail for use_function_calling=True: the model is forced
1332
- # to call the parser tool (React_ / FewShot / etc.) and fills
1333
- # in `action` + `action_input`. Nothing in the parser schema
1334
- # constrains `action` to a real tool name, so the model can
1335
- # (and does) put its natural-language closing response there
1336
- # e.g. action = "Provide a response to the athlete."
1337
- # Without this guardrail the runner would try to dispatch
1338
- # a tool with that name, get "not found", and the turn dies.
1339
- # Detection: action isn't Final_Answer AND isn't in the
1340
- # registry. Treat the whole turn as an implicit Final_Answer
1341
- # built from whatever text the model provided (thought +
1342
- # action + action_input, whichever are present).
1364
+ # Guardrail: the model emitted an `action` value that is
1365
+ # neither a terminal-answer variant NOR a registered tool.
1366
+ # Two scenarios trigger this:
1367
+ # 1. use_function_calling=True: the parser tool schema
1368
+ # accepts any string, so the model stuffs its natural-
1369
+ # language closing text into `action` (e.g. "Provide a
1370
+ # response to the athlete."). Without this guard, the
1371
+ # runner would dispatch it as a tool, get "not found",
1372
+ # and the turn dies.
1373
+ # 2. action="" (empty). Under FC, the model sometimes
1374
+ # omits `action` entirely; Pydantic falls back to the
1375
+ # field default. Same failure empty tool name.
1376
+ # Route both to Final_Answer, built from whatever text the
1377
+ # model actually provided (thought + action + action_input).
1343
1378
  known_tools = set(self.func) | set(self.args)
1344
- if action and action != "Final_Answer" and action not in known_tools:
1379
+ if not action or action not in known_tools:
1345
1380
  parts: List[str] = []
1346
1381
  if thought:
1347
1382
  parts.append(str(thought))
1348
- # action itself may BE the response text
1349
- if action:
1383
+ # Only include `action` in the reply if it looks like
1384
+ # real response text (not empty, not a suspicious short
1385
+ # sentinel like "answer"). Skip empty.
1386
+ if action and action.strip():
1350
1387
  parts.append(str(action))
1351
- # action_input often carries the actual body
1352
1388
  if action_input and str(action_input).strip():
1353
1389
  parts.append(str(action_input))
1354
1390
  final_answer = "\n\n".join(parts) or (
1355
1391
  "(agent emitted an unrecognized action and no text)"
1356
1392
  )
1357
1393
  if self.verbose:
1394
+ preview = (action or "<empty>")[:60]
1358
1395
  print(
1359
1396
  f"\x1B[1;33m[loop] implicit-final: action "
1360
- f"'{action[:60]}' is not a registered tool; "
1361
- f"treating turn as Final_Answer\x1B[0m"
1397
+ f"'{preview}' is not a registered tool and not "
1398
+ f"a Final_Answer variant; treating turn as "
1399
+ f"Final_Answer\x1B[0m"
1362
1400
  )
1363
1401
  yield {"type": "final", "content": final_answer}
1364
1402
  break
@@ -13,7 +13,7 @@ from agentx_dev.ChatModel import BaseChatModel
13
13
  from agentx_dev.Agents.Agent import StandardParser, ToolCall, ToolError
14
14
  from agentx_dev.Tools import StandardTool, StructuredTool, logger
15
15
  from agentx_dev.AsyncTools import AsyncStandardTool, AsyncStructuredTool
16
- from agentx_dev.Runner.AgentRun import ToolRegistry
16
+ from agentx_dev.Runner.AgentRun import ToolRegistry, _is_terminal_action
17
17
  from typing import Dict, Callable, List, Type, Optional, Any, AsyncIterator
18
18
  from pydantic import BaseModel, Field
19
19
  import asyncio
@@ -493,7 +493,10 @@ class AsyncAgentRunner:
493
493
  step_description = f"Step {count}: {action} with {action_input}"
494
494
  steps.append(step_description)
495
495
 
496
- if action == "Final_Answer":
496
+ # Terminal-action check — tolerant of format variants that
497
+ # LLMs emit under use_function_calling=True where the schema
498
+ # doesn't constrain `action` to a specific spelling.
499
+ if _is_terminal_action(action):
497
500
  final_answer = action_input
498
501
  break
499
502
 
@@ -503,6 +506,32 @@ class AsyncAgentRunner:
503
506
  elif thought:
504
507
  logger.info(f"thought: {thought}")
505
508
 
509
+ # Implicit-final guardrail: `action` is empty OR not a
510
+ # registered tool AND not a Final_Answer variant. Route to
511
+ # Final_Answer built from the text the model provided.
512
+ known_tools = set(self.registry.sync_std) | set(self.registry.sync_struct) \
513
+ | set(self.registry.async_std) | set(self.registry.async_struct)
514
+ if not action or action not in known_tools:
515
+ parts: List[str] = []
516
+ if thought:
517
+ parts.append(str(thought))
518
+ if action and action.strip():
519
+ parts.append(str(action))
520
+ if action_input and str(action_input).strip():
521
+ parts.append(str(action_input))
522
+ final_answer = "\n\n".join(parts) or (
523
+ "(agent emitted an unrecognized action and no text)"
524
+ )
525
+ if self.verbose:
526
+ preview = (action or "<empty>")[:60]
527
+ print(
528
+ f"\x1B[1;33m[loop] implicit-final: action "
529
+ f"'{preview}' is not a registered tool and not "
530
+ f"a Final_Answer variant; treating turn as "
531
+ f"Final_Answer\x1B[0m"
532
+ )
533
+ break
534
+
506
535
  if self.verbose:
507
536
  print(f"\x1B[3;33m[tool] Invoking '{action}' with args: {action_input}\x1B[0m")
508
537
  else:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentx-dev
3
- Version: 3.0.2
3
+ Version: 3.0.3
4
4
  Summary: A lightweight LLM agent framework with multi-agent orchestration (Supervisor + dynamic specialist spawning), permission-gated default tools, MCP integration, and prompt-template presets.
5
5
  Author-email: Bruce-Arhin Shadrach <brucearhin098@gmail.com>
6
6
  License: MIT License
@@ -1,4 +1,3 @@
1
1
  agentx_dev
2
2
  dist
3
3
  examples
4
- test_dl
@@ -1,7 +1,7 @@
1
1
  [project]
2
2
 
3
3
  name = "agentx-dev"
4
- version = "3.0.2"
4
+ version = "3.0.3"
5
5
  description = "A lightweight LLM agent framework with multi-agent orchestration (Supervisor + dynamic specialist spawning), permission-gated default tools, MCP integration, and prompt-template presets."
6
6
  authors = [{ name = "Bruce-Arhin Shadrach", email = "brucearhin098@gmail.com" }]
7
7
  # Best practice: link to the license file
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes