flowforge-sdk 0.3.2__tar.gz → 0.3.4__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 (26) hide show
  1. {flowforge_sdk-0.3.2 → flowforge_sdk-0.3.4}/PKG-INFO +1 -1
  2. {flowforge_sdk-0.3.2 → flowforge_sdk-0.3.4}/pyproject.toml +1 -1
  3. {flowforge_sdk-0.3.2 → flowforge_sdk-0.3.4}/src/flowforge/__init__.py +3 -1
  4. {flowforge_sdk-0.3.2 → flowforge_sdk-0.3.4}/src/flowforge/steps.py +94 -1
  5. {flowforge_sdk-0.3.2 → flowforge_sdk-0.3.4}/src/flowforge/tools.py +113 -1
  6. {flowforge_sdk-0.3.2 → flowforge_sdk-0.3.4}/.gitignore +0 -0
  7. {flowforge_sdk-0.3.2 → flowforge_sdk-0.3.4}/README.md +0 -0
  8. {flowforge_sdk-0.3.2 → flowforge_sdk-0.3.4}/src/flowforge/agent.py +0 -0
  9. {flowforge_sdk-0.3.2 → flowforge_sdk-0.3.4}/src/flowforge/agent_def.py +0 -0
  10. {flowforge_sdk-0.3.2 → flowforge_sdk-0.3.4}/src/flowforge/ai/__init__.py +0 -0
  11. {flowforge_sdk-0.3.2 → flowforge_sdk-0.3.4}/src/flowforge/ai/providers.py +0 -0
  12. {flowforge_sdk-0.3.2 → flowforge_sdk-0.3.4}/src/flowforge/client.py +0 -0
  13. {flowforge_sdk-0.3.2 → flowforge_sdk-0.3.4}/src/flowforge/config.py +0 -0
  14. {flowforge_sdk-0.3.2 → flowforge_sdk-0.3.4}/src/flowforge/context.py +0 -0
  15. {flowforge_sdk-0.3.2 → flowforge_sdk-0.3.4}/src/flowforge/decorators.py +0 -0
  16. {flowforge_sdk-0.3.2 → flowforge_sdk-0.3.4}/src/flowforge/dev/__init__.py +0 -0
  17. {flowforge_sdk-0.3.2 → flowforge_sdk-0.3.4}/src/flowforge/dev/server.py +0 -0
  18. {flowforge_sdk-0.3.2 → flowforge_sdk-0.3.4}/src/flowforge/exceptions.py +0 -0
  19. {flowforge_sdk-0.3.2 → flowforge_sdk-0.3.4}/src/flowforge/execution.py +0 -0
  20. {flowforge_sdk-0.3.2 → flowforge_sdk-0.3.4}/src/flowforge/integrations/__init__.py +0 -0
  21. {flowforge_sdk-0.3.2 → flowforge_sdk-0.3.4}/src/flowforge/integrations/fastapi.py +0 -0
  22. {flowforge_sdk-0.3.2 → flowforge_sdk-0.3.4}/src/flowforge/network.py +0 -0
  23. {flowforge_sdk-0.3.2 → flowforge_sdk-0.3.4}/src/flowforge/router.py +0 -0
  24. {flowforge_sdk-0.3.2 → flowforge_sdk-0.3.4}/src/flowforge/streaming.py +0 -0
  25. {flowforge_sdk-0.3.2 → flowforge_sdk-0.3.4}/src/flowforge/triggers.py +0 -0
  26. {flowforge_sdk-0.3.2 → flowforge_sdk-0.3.4}/src/flowforge/worker.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: flowforge-sdk
3
- Version: 0.3.2
3
+ Version: 0.3.4
4
4
  Summary: Python SDK for FlowForge - AI workflow orchestration
5
5
  Project-URL: Homepage, https://github.com/flowforge/flowforge
6
6
  Project-URL: Documentation, https://flowforge.dev/docs
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "flowforge-sdk"
3
- version = "0.3.2"
3
+ version = "0.3.4"
4
4
  description = "Python SDK for FlowForge - AI workflow orchestration"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -27,7 +27,7 @@ from flowforge.exceptions import (
27
27
  from flowforge.network import Network, NetworkResult, NetworkState, RouterContext, network
28
28
  from flowforge.steps import step
29
29
  from flowforge.streaming import RunEvent, RunEventType
30
- from flowforge.tools import Tool, tool
30
+ from flowforge.tools import SubAgentConfig, Tool, sub_agent, tool
31
31
  from flowforge.triggers import Trigger, trigger
32
32
 
33
33
  __version__ = "0.1.0"
@@ -45,6 +45,8 @@ __all__ = [
45
45
  # Tools
46
46
  "Tool",
47
47
  "tool",
48
+ "sub_agent",
49
+ "SubAgentConfig",
48
50
  # Agent
49
51
  "AgentConfig",
50
52
  "AgentState",
@@ -11,7 +11,7 @@ from flowforge.agent_def import AgentDefinition
11
11
  from flowforge.exceptions import StepCompleted, StepFailed
12
12
  from flowforge.network import Network, NetworkResult, NetworkState, RouterContext
13
13
  from flowforge.router import LLMRouter
14
- from flowforge.tools import Tool
14
+ from flowforge.tools import SubAgentConfig, Tool
15
15
 
16
16
  T = TypeVar("T")
17
17
 
@@ -458,6 +458,8 @@ class StepManager:
458
458
  checkpoint_strategy: str = "per_tool",
459
459
  max_tool_calls: int = 50,
460
460
  temperature: float = 0.7,
461
+ _depth: int = 0,
462
+ _max_depth: int = 3,
461
463
  **kwargs: Any,
462
464
  ) -> AgentResult:
463
465
  """
@@ -467,6 +469,9 @@ class StepManager:
467
469
  until the task is complete or limits are reached. Each tool execution
468
470
  is checkpointed for durability.
469
471
 
472
+ Sub-agent tools (created via ``sub_agent()``) are automatically
473
+ detected and executed as nested agent loops.
474
+
470
475
  Args:
471
476
  step_id: Unique identifier for this agent execution.
472
477
  task: The task/prompt for the agent to accomplish.
@@ -480,6 +485,8 @@ class StepManager:
480
485
  - "final_only": Only checkpoint final result
481
486
  max_tool_calls: Maximum total tool calls across iterations (default 50).
482
487
  temperature: Sampling temperature for LLM (default 0.7).
488
+ _depth: Current nesting depth (internal, used by sub-agent recursion).
489
+ _max_depth: Maximum allowed nesting depth (default 3).
483
490
  **kwargs: Additional LLM parameters.
484
491
 
485
492
  Returns:
@@ -667,6 +674,88 @@ class StepManager:
667
674
  # Execute tool via step.run for checkpointing
668
675
  tool_step_id = f"{step_id}/iter-{state.iteration}/tool-{tool_call_id}"
669
676
 
677
+ # --- Sub-agent interception ---
678
+ # If the tool is a sub-agent wrapper, run a nested agent
679
+ # loop instead of calling tool.fn directly.
680
+ cfg: SubAgentConfig | None = getattr(tool, "_sub_agent_config", None)
681
+ if cfg is not None:
682
+ # Depth guard: prevent infinite sub-agent recursion
683
+ if _depth >= _max_depth:
684
+ error_message = json.dumps({
685
+ "error": f"Sub-agent depth limit reached ({_max_depth}). "
686
+ "Cannot spawn further sub-agents.",
687
+ })
688
+ state.messages.append({
689
+ "role": "tool",
690
+ "tool_call_id": tool_call_id,
691
+ "name": tool_name,
692
+ "content": error_message,
693
+ })
694
+ tool_call_record["status"] = "failed"
695
+ tool_call_record["error"] = "depth_limit_reached"
696
+ state.tool_calls_count += 1
697
+ continue
698
+
699
+ sub_step_id = f"{step_id}/sub-{cfg.agent.name}-{tool_call_id}"
700
+
701
+ # Build sub-agent system prompt with optional parent context
702
+ sub_system = cfg.agent.system
703
+ if cfg.context_mode == "summary":
704
+ recent = state.messages[-6:] # last 3 exchanges
705
+ context_lines = [
706
+ f"[{m['role']}]: {str(m.get('content', ''))[:200]}"
707
+ for m in recent
708
+ ]
709
+ sub_system += "\n\nParent context:\n" + "\n".join(context_lines)
710
+ elif cfg.context_mode == "full_history":
711
+ sub_system += "\n\nFull parent conversation:\n" + json.dumps(state.messages)
712
+
713
+ try:
714
+ sub_result = await self.agent(
715
+ sub_step_id,
716
+ task=tool_args.get("task", ""),
717
+ model=cfg.agent.model or model,
718
+ system=sub_system,
719
+ tools=cfg.agent.tools,
720
+ max_iterations=cfg.max_iterations,
721
+ max_tool_calls=cfg.max_tool_calls,
722
+ temperature=cfg.temperature,
723
+ _depth=_depth + 1,
724
+ _max_depth=_max_depth,
725
+ **kwargs,
726
+ )
727
+ sub_output = sub_result.output
728
+
729
+ state.messages.append({
730
+ "role": "tool",
731
+ "tool_call_id": tool_call_id,
732
+ "name": tool_name,
733
+ "content": sub_output,
734
+ })
735
+ tool_call_record["status"] = "success"
736
+ tool_call_record["result"] = sub_output
737
+ tool_call_record["sub_agent"] = sub_result.to_dict()
738
+ except StepCompleted:
739
+ # Sub-agent completed its first execution — propagate
740
+ # so the server can memoize the step. On replay the
741
+ # nested agent() call returns normally.
742
+ raise
743
+ except (StepFailed, Exception) as e:
744
+ err_msg = str(e)
745
+ error_message = json.dumps({"error": err_msg})
746
+ state.messages.append({
747
+ "role": "tool",
748
+ "tool_call_id": tool_call_id,
749
+ "name": tool_name,
750
+ "content": error_message,
751
+ })
752
+ tool_call_record["status"] = "failed"
753
+ tool_call_record["error"] = err_msg
754
+
755
+ state.tool_calls_count += 1
756
+ continue
757
+ # --- End sub-agent interception ---
758
+
670
759
  try:
671
760
  tool_result = await self.run(
672
761
  tool_step_id,
@@ -1129,6 +1218,8 @@ class _StepProxy:
1129
1218
  checkpoint_strategy: str = "per_tool",
1130
1219
  max_tool_calls: int = 50,
1131
1220
  temperature: float = 0.7,
1221
+ _depth: int = 0,
1222
+ _max_depth: int = 3,
1132
1223
  **kwargs: Any,
1133
1224
  ) -> AgentResult:
1134
1225
  return await self._get_manager().agent(
@@ -1141,6 +1232,8 @@ class _StepProxy:
1141
1232
  checkpoint_strategy=checkpoint_strategy,
1142
1233
  max_tool_calls=max_tool_calls,
1143
1234
  temperature=temperature,
1235
+ _depth=_depth,
1236
+ _max_depth=_max_depth,
1144
1237
  **kwargs,
1145
1238
  )
1146
1239
 
@@ -1,10 +1,15 @@
1
1
  """Tool definition API for FlowForge agents."""
2
2
 
3
+ from __future__ import annotations
4
+
3
5
  import inspect
4
6
  import types as _builtintypes
5
7
  from collections.abc import Awaitable, Callable
6
8
  from dataclasses import dataclass, field
7
- from typing import Any, Literal, Union, get_args, get_origin, get_type_hints
9
+ from typing import TYPE_CHECKING, Any, Literal, Union, get_args, get_origin, get_type_hints
10
+
11
+ if TYPE_CHECKING:
12
+ from flowforge.agent_def import AgentDefinition
8
13
 
9
14
 
10
15
  @dataclass
@@ -270,3 +275,110 @@ def tool(
270
275
  )
271
276
 
272
277
  return decorator
278
+
279
+
280
+ @dataclass
281
+ class SubAgentConfig:
282
+ """Configuration for a sub-agent tool.
283
+
284
+ This is attached to a Tool via ``_sub_agent_config`` so that
285
+ ``step.agent()`` can detect the tool as a sub-agent delegation
286
+ and run a nested agent loop instead of calling ``tool.fn``.
287
+
288
+ Attributes:
289
+ agent: The agent definition to run as a sub-agent.
290
+ max_iterations: Max iterations for the sub-agent loop.
291
+ max_tool_calls: Max tool calls for the sub-agent loop.
292
+ temperature: Sampling temperature for the sub-agent LLM.
293
+ context_mode: How much parent context to share with the sub-agent.
294
+ """
295
+
296
+ agent: AgentDefinition
297
+ max_iterations: int = 20
298
+ max_tool_calls: int = 50
299
+ temperature: float = 0.7
300
+ context_mode: Literal["task_only", "summary", "full_history"] = "task_only"
301
+
302
+
303
+ def sub_agent(
304
+ agent: AgentDefinition,
305
+ *,
306
+ description: str | None = None,
307
+ max_iterations: int = 20,
308
+ max_tool_calls: int = 50,
309
+ temperature: float = 0.7,
310
+ context_mode: Literal["task_only", "summary", "full_history"] = "task_only",
311
+ ) -> Tool:
312
+ """Create a Tool that delegates work to a sub-agent.
313
+
314
+ When the parent agent's LLM calls this tool, ``step.agent()``
315
+ intercepts the call and runs a nested agent loop with the
316
+ given agent definition, returning the sub-agent's output as
317
+ the tool result.
318
+
319
+ Args:
320
+ agent: Agent definition describing the sub-agent.
321
+ description: Tool description shown to the LLM. If omitted,
322
+ auto-generated from the agent name and system prompt.
323
+ max_iterations: Maximum reasoning iterations for the sub-agent.
324
+ max_tool_calls: Maximum tool calls for the sub-agent.
325
+ temperature: Sampling temperature for the sub-agent.
326
+ context_mode: How much parent context to pass:
327
+ - "task_only" (default): Only the delegated task string.
328
+ - "summary": Task + last 3 parent exchanges as context.
329
+ - "full_history": Task + full parent conversation.
330
+
331
+ Returns:
332
+ A Tool with a single ``task: str`` parameter and a
333
+ ``_sub_agent_config`` attribute for detection.
334
+
335
+ Example:
336
+ researcher = agent_def(
337
+ name="researcher",
338
+ system="You research topics thoroughly.",
339
+ tools=[web_search],
340
+ )
341
+
342
+ research_tool = sub_agent(researcher, description="Delegate research tasks.")
343
+
344
+ result = await step.agent(
345
+ "manager",
346
+ task="Plan a project",
347
+ model="claude-opus-4-6",
348
+ tools=[research_tool],
349
+ )
350
+ """
351
+ desc = description or f"Delegate a task to the '{agent.name}' sub-agent. {agent.system[:100]}"
352
+
353
+ async def _placeholder(task: str) -> str: # noqa: ARG001
354
+ raise RuntimeError(
355
+ "Sub-agent tool should not be called directly. "
356
+ "It is intercepted by step.agent()."
357
+ )
358
+
359
+ t = Tool(
360
+ name=agent.name,
361
+ description=desc,
362
+ fn=_placeholder,
363
+ parameters={
364
+ "type": "object",
365
+ "properties": {
366
+ "task": {
367
+ "type": "string",
368
+ "description": "The task to delegate to the sub-agent.",
369
+ },
370
+ },
371
+ "required": ["task"],
372
+ },
373
+ )
374
+
375
+ # Attach sub-agent config as a marker for step.agent() detection
376
+ t._sub_agent_config = SubAgentConfig( # type: ignore[attr-defined]
377
+ agent=agent,
378
+ max_iterations=max_iterations,
379
+ max_tool_calls=max_tool_calls,
380
+ temperature=temperature,
381
+ context_mode=context_mode,
382
+ )
383
+
384
+ return t
File without changes
File without changes