flw-studio 0.1.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.
- agentic_flow/__init__.py +186 -0
- agentic_flow/cli.py +232 -0
- agentic_flow/console.py +114 -0
- agentic_flow/core/__init__.py +6 -0
- agentic_flow/core/agent.py +357 -0
- agentic_flow/core/authority.py +210 -0
- agentic_flow/core/checkpoint.py +162 -0
- agentic_flow/core/context.py +59 -0
- agentic_flow/core/cursor_format.py +208 -0
- agentic_flow/core/events.py +181 -0
- agentic_flow/core/graph.py +657 -0
- agentic_flow/core/limits.py +97 -0
- agentic_flow/core/providers/__init__.py +27 -0
- agentic_flow/core/providers/anthropic_provider.py +103 -0
- agentic_flow/core/providers/base.py +128 -0
- agentic_flow/core/providers/gemini_provider.py +157 -0
- agentic_flow/core/retry.py +92 -0
- agentic_flow/core/runtime.py +883 -0
- agentic_flow/core/schema.py +118 -0
- agentic_flow/core/secrets.py +241 -0
- agentic_flow/core/store.py +799 -0
- agentic_flow/core/templating.py +82 -0
- agentic_flow/core/tools/__init__.py +31 -0
- agentic_flow/core/tools/builtin/__init__.py +27 -0
- agentic_flow/core/tools/builtin/messaging.py +50 -0
- agentic_flow/core/tools/builtin/state.py +47 -0
- agentic_flow/core/tools/builtin/todos.py +115 -0
- agentic_flow/core/tools/core.py +282 -0
- agentic_flow/core/tools/mcp.py +176 -0
- agentic_flow/loader.py +1130 -0
- agentic_flow/log.py +108 -0
- agentic_flow/pipeline.py +145 -0
- agentic_flow/program.py +256 -0
- flw_studio-0.1.0.dist-info/METADATA +475 -0
- flw_studio-0.1.0.dist-info/RECORD +39 -0
- flw_studio-0.1.0.dist-info/WHEEL +5 -0
- flw_studio-0.1.0.dist-info/entry_points.txt +2 -0
- flw_studio-0.1.0.dist-info/licenses/LICENSE +21 -0
- flw_studio-0.1.0.dist-info/top_level.txt +1 -0
agentic_flow/__init__.py
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
"""agentic_flow: define agents (tools + system prompt) in YAML, run them as a pipeline."""
|
|
2
|
+
import logging as _logging
|
|
3
|
+
|
|
4
|
+
# Library default: attach a NullHandler to the package logger so a run is silent until
|
|
5
|
+
# the application calls configure_logging (or adds its own handler). Done here — not as
|
|
6
|
+
# an import-time side effect of the layer-2 `log` module — so it runs on any
|
|
7
|
+
# `agentic_flow.*` import and the engine never depends on `log` for it (ADR 0004).
|
|
8
|
+
_logging.getLogger("agentic_flow").addHandler(_logging.NullHandler())
|
|
9
|
+
|
|
10
|
+
from .core.authority import ReplanRejected, maybe_replan, parse_delta, validate_replan
|
|
11
|
+
from .core.agent import (
|
|
12
|
+
DEFAULT_MODEL,
|
|
13
|
+
DEFAULT_PROVIDER,
|
|
14
|
+
Agent,
|
|
15
|
+
AgentError,
|
|
16
|
+
AgentRefusal,
|
|
17
|
+
AgentTimeout,
|
|
18
|
+
MaxStepsExceeded,
|
|
19
|
+
OutputParseError,
|
|
20
|
+
TokenBudgetExceeded,
|
|
21
|
+
)
|
|
22
|
+
from .core.tools.builtin import register_builtins # registers shared-state tools on the default registry
|
|
23
|
+
from .core.checkpoint import (
|
|
24
|
+
Checkpointer,
|
|
25
|
+
InMemoryCheckpointer,
|
|
26
|
+
JsonFileCheckpointer,
|
|
27
|
+
get_checkpointer,
|
|
28
|
+
register_checkpointer,
|
|
29
|
+
)
|
|
30
|
+
from .console import console_tracer, print_final_state
|
|
31
|
+
from .core.context import Context
|
|
32
|
+
from .core.cursor_format import CheckpointRecord, Pending
|
|
33
|
+
from .core.events import EventBus, Subscription
|
|
34
|
+
from .core.graph import (
|
|
35
|
+
END,
|
|
36
|
+
AddEdge,
|
|
37
|
+
AddNode,
|
|
38
|
+
AgentBody,
|
|
39
|
+
Authority,
|
|
40
|
+
ConditionalEdge,
|
|
41
|
+
Graph,
|
|
42
|
+
GraphError,
|
|
43
|
+
HumanBody,
|
|
44
|
+
ModelDriven,
|
|
45
|
+
Node,
|
|
46
|
+
Prune,
|
|
47
|
+
Replan,
|
|
48
|
+
ReplanDelta,
|
|
49
|
+
StaticEdge,
|
|
50
|
+
SubgraphBody,
|
|
51
|
+
ToolBody,
|
|
52
|
+
render_graph,
|
|
53
|
+
)
|
|
54
|
+
from .core.limits import Limits
|
|
55
|
+
from .loader import LoadedProgram, LoaderError, build_program, lower_pipeline
|
|
56
|
+
from .log import configure_logging, event_logger, logger
|
|
57
|
+
from .pipeline import Loop, Step
|
|
58
|
+
from .program import Orchestrator
|
|
59
|
+
from .core.providers import (
|
|
60
|
+
AnthropicProvider,
|
|
61
|
+
Completion,
|
|
62
|
+
GeminiProvider,
|
|
63
|
+
Provider,
|
|
64
|
+
get_provider,
|
|
65
|
+
register_provider,
|
|
66
|
+
)
|
|
67
|
+
from .core.retry import RetryPolicy, run_with_retry
|
|
68
|
+
from .core.runtime import (
|
|
69
|
+
Budget,
|
|
70
|
+
FrontierDeadlock,
|
|
71
|
+
HumanInputRequired,
|
|
72
|
+
MaxVisitsExceeded,
|
|
73
|
+
NodeOutputError,
|
|
74
|
+
NodePaused,
|
|
75
|
+
RouteOutOfSet,
|
|
76
|
+
RunStepsExceeded,
|
|
77
|
+
RunTimeout,
|
|
78
|
+
RuntimeFailure,
|
|
79
|
+
resolve_out_edges,
|
|
80
|
+
resolve_route,
|
|
81
|
+
)
|
|
82
|
+
from .core.runtime import resume as resume_graph
|
|
83
|
+
from .core.runtime import run as run_graph
|
|
84
|
+
from .core.store import (
|
|
85
|
+
REDUCERS,
|
|
86
|
+
Channel,
|
|
87
|
+
MessageLog,
|
|
88
|
+
Reducer,
|
|
89
|
+
ReducerTypeError,
|
|
90
|
+
Store,
|
|
91
|
+
TodoBoard,
|
|
92
|
+
UnsupportedSnapshotVersion,
|
|
93
|
+
)
|
|
94
|
+
from .core.tools import Tool, ToolRegistry, registry, tool
|
|
95
|
+
|
|
96
|
+
__all__ = [
|
|
97
|
+
"AddEdge",
|
|
98
|
+
"AddNode",
|
|
99
|
+
"Agent",
|
|
100
|
+
"AgentBody",
|
|
101
|
+
"AgentError",
|
|
102
|
+
"AgentRefusal",
|
|
103
|
+
"AgentTimeout",
|
|
104
|
+
"AnthropicProvider",
|
|
105
|
+
"Authority",
|
|
106
|
+
"Budget",
|
|
107
|
+
"Channel",
|
|
108
|
+
"CheckpointRecord",
|
|
109
|
+
"Checkpointer",
|
|
110
|
+
"Completion",
|
|
111
|
+
"ConditionalEdge",
|
|
112
|
+
"Context",
|
|
113
|
+
"DEFAULT_MODEL",
|
|
114
|
+
"DEFAULT_PROVIDER",
|
|
115
|
+
"END",
|
|
116
|
+
"EventBus",
|
|
117
|
+
"FrontierDeadlock",
|
|
118
|
+
"GeminiProvider",
|
|
119
|
+
"Graph",
|
|
120
|
+
"GraphError",
|
|
121
|
+
"HumanBody",
|
|
122
|
+
"HumanInputRequired",
|
|
123
|
+
"InMemoryCheckpointer",
|
|
124
|
+
"JsonFileCheckpointer",
|
|
125
|
+
"Limits",
|
|
126
|
+
"LoadedProgram",
|
|
127
|
+
"LoaderError",
|
|
128
|
+
"Loop",
|
|
129
|
+
"MaxStepsExceeded",
|
|
130
|
+
"MaxVisitsExceeded",
|
|
131
|
+
"MessageLog",
|
|
132
|
+
"ModelDriven",
|
|
133
|
+
"Node",
|
|
134
|
+
"NodeOutputError",
|
|
135
|
+
"NodePaused",
|
|
136
|
+
"Orchestrator",
|
|
137
|
+
"OutputParseError",
|
|
138
|
+
"Pending",
|
|
139
|
+
"Provider",
|
|
140
|
+
"Prune",
|
|
141
|
+
"REDUCERS",
|
|
142
|
+
"Reducer",
|
|
143
|
+
"ReducerTypeError",
|
|
144
|
+
"Replan",
|
|
145
|
+
"ReplanDelta",
|
|
146
|
+
"ReplanRejected",
|
|
147
|
+
"RetryPolicy",
|
|
148
|
+
"RouteOutOfSet",
|
|
149
|
+
"RunStepsExceeded",
|
|
150
|
+
"RunTimeout",
|
|
151
|
+
"RuntimeFailure",
|
|
152
|
+
"StaticEdge",
|
|
153
|
+
"Step",
|
|
154
|
+
"Store",
|
|
155
|
+
"SubgraphBody",
|
|
156
|
+
"TodoBoard",
|
|
157
|
+
"UnsupportedSnapshotVersion",
|
|
158
|
+
"Subscription",
|
|
159
|
+
"Tool",
|
|
160
|
+
"ToolBody",
|
|
161
|
+
"render_graph",
|
|
162
|
+
"TokenBudgetExceeded",
|
|
163
|
+
"ToolRegistry",
|
|
164
|
+
"build_program",
|
|
165
|
+
"configure_logging",
|
|
166
|
+
"console_tracer",
|
|
167
|
+
"event_logger",
|
|
168
|
+
"print_final_state",
|
|
169
|
+
"get_checkpointer",
|
|
170
|
+
"get_provider",
|
|
171
|
+
"logger",
|
|
172
|
+
"lower_pipeline",
|
|
173
|
+
"maybe_replan",
|
|
174
|
+
"parse_delta",
|
|
175
|
+
"register_checkpointer",
|
|
176
|
+
"register_provider",
|
|
177
|
+
"register_builtins",
|
|
178
|
+
"registry",
|
|
179
|
+
"resolve_out_edges",
|
|
180
|
+
"resolve_route",
|
|
181
|
+
"resume_graph",
|
|
182
|
+
"run_graph",
|
|
183
|
+
"run_with_retry",
|
|
184
|
+
"tool",
|
|
185
|
+
"validate_replan",
|
|
186
|
+
]
|
agentic_flow/cli.py
ADDED
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
"""Command-line entry point: run a pipeline YAML.
|
|
2
|
+
|
|
3
|
+
agentic-flow path/to/pipeline.yaml --tools mypkg.tools
|
|
4
|
+
agentic-flow examples/shared_state/pipeline.yaml --tools examples.shared_state.tools --dry-run
|
|
5
|
+
|
|
6
|
+
``--tools`` imports modules that register ``@tool`` functions (their import is the
|
|
7
|
+
side effect that populates the registry). ``--set KEY=VALUE`` seeds initial state.
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import argparse
|
|
12
|
+
import importlib
|
|
13
|
+
import json
|
|
14
|
+
import logging
|
|
15
|
+
import sys
|
|
16
|
+
import uuid
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
from .core.checkpoint import get_checkpointer
|
|
20
|
+
from .console import console_tracer, print_final_state
|
|
21
|
+
from .core.graph import KIND_HUMAN, render_graph
|
|
22
|
+
from .log import configure_logging, event_logger
|
|
23
|
+
from .program import Orchestrator
|
|
24
|
+
from .core.runtime import HumanInputRequired, NodePaused
|
|
25
|
+
from .core.tools import registry
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _has_human_node(graph) -> bool:
|
|
29
|
+
"""True if ``graph`` contains a ``human:`` (pause) node — such a run is only
|
|
30
|
+
resumable with a durable checkpoint."""
|
|
31
|
+
return any(node.kind == KIND_HUMAN for node in graph.nodes.values())
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _resume_command(pipeline: str, spec: str, run_id: str) -> str:
|
|
35
|
+
"""A copy-pasteable command to resume this paused run (the human fills in the answer)."""
|
|
36
|
+
return (
|
|
37
|
+
f"agentic-flow {pipeline} --checkpoint {spec} --run-id {run_id} "
|
|
38
|
+
f'--resume --human-input "<your answer>"'
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _print_pause(prompt: str, pipeline: str, spec: str, run_id: str) -> None:
|
|
43
|
+
"""Surface a pause: the rendered prompt + how to resume. (Exit code 2 follows.)"""
|
|
44
|
+
print(f"\n[paused] run {run_id!r} awaits human input:")
|
|
45
|
+
print(f" {prompt}")
|
|
46
|
+
print(f" resume: {_resume_command(pipeline, spec, run_id)}")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _resolve_human_input(args, parser) -> object | None:
|
|
50
|
+
"""The human's answer from at most one of the three --human-input* flags.
|
|
51
|
+
|
|
52
|
+
Returns the string / parsed-JSON value / file contents, or ``None`` when none
|
|
53
|
+
is supplied (the "not provided" sentinel — resume then prompts on a TTY or
|
|
54
|
+
continues straight through for a crash-resume). More than one is a usage error.
|
|
55
|
+
"""
|
|
56
|
+
given = [
|
|
57
|
+
name for name, value in (
|
|
58
|
+
("--human-input", args.human_input),
|
|
59
|
+
("--human-input-json", args.human_input_json),
|
|
60
|
+
("--human-input-file", args.human_input_file),
|
|
61
|
+
) if value is not None
|
|
62
|
+
]
|
|
63
|
+
if len(given) > 1:
|
|
64
|
+
parser.error(f"only one of {', '.join(given)} may be given")
|
|
65
|
+
if args.human_input is not None:
|
|
66
|
+
return args.human_input
|
|
67
|
+
if args.human_input_json is not None:
|
|
68
|
+
try:
|
|
69
|
+
return json.loads(args.human_input_json)
|
|
70
|
+
except json.JSONDecodeError as exc:
|
|
71
|
+
parser.error(f"--human-input-json is not valid JSON: {exc}")
|
|
72
|
+
if args.human_input_file is not None:
|
|
73
|
+
return Path(args.human_input_file).read_text(encoding="utf-8")
|
|
74
|
+
return None
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _run_to_exit(produce_state, *, pipeline: str, spec: str | None, run_id: str | None) -> int:
|
|
78
|
+
"""Run ``produce_state()`` and map the engine's signals to a process exit code.
|
|
79
|
+
|
|
80
|
+
The single place the run/resume outcomes become exit codes: a ``human:`` pause
|
|
81
|
+
(``NodePaused``) or a non-interactive resume that still needs an answer
|
|
82
|
+
(``HumanInputRequired``) → 2 (print how to resume); any real failure → 1
|
|
83
|
+
(message on stderr); success → 0 (print the final state). Shared by all three
|
|
84
|
+
CLI run paths (plain, durable, resume) so the mapping lives once.
|
|
85
|
+
"""
|
|
86
|
+
try:
|
|
87
|
+
state = produce_state()
|
|
88
|
+
except (NodePaused, HumanInputRequired) as paused:
|
|
89
|
+
_print_pause(paused.prompt, pipeline, spec, run_id)
|
|
90
|
+
return 2
|
|
91
|
+
except Exception as exc: # any real failure → exit 1
|
|
92
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
93
|
+
return 1
|
|
94
|
+
print_final_state(state)
|
|
95
|
+
return 0
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def main(argv: list[str] | None = None) -> int:
|
|
99
|
+
parser = argparse.ArgumentParser(prog="agentic-flow", description=__doc__.splitlines()[0])
|
|
100
|
+
parser.add_argument("pipeline", help="Path to a pipeline YAML file.")
|
|
101
|
+
parser.add_argument(
|
|
102
|
+
"-t", "--tools", action="append", default=[], metavar="MODULE",
|
|
103
|
+
help="Import a module that registers @tool functions (repeatable).",
|
|
104
|
+
)
|
|
105
|
+
parser.add_argument(
|
|
106
|
+
"--set", action="append", default=[], metavar="KEY=VALUE",
|
|
107
|
+
help="Seed an initial state value (repeatable).",
|
|
108
|
+
)
|
|
109
|
+
parser.add_argument(
|
|
110
|
+
"--dry-run", action="store_true",
|
|
111
|
+
help="Build and print the pipeline without calling any model.",
|
|
112
|
+
)
|
|
113
|
+
parser.add_argument(
|
|
114
|
+
"--log-level", metavar="LEVEL", default=None,
|
|
115
|
+
help="Trace through Python logging at this level (DEBUG/INFO/WARNING/…) "
|
|
116
|
+
"instead of the default console printer.",
|
|
117
|
+
)
|
|
118
|
+
parser.add_argument(
|
|
119
|
+
"--checkpoint", metavar="SPEC", default=None,
|
|
120
|
+
help="Persist the run durably via a named backend, e.g. 'json:./runs' or "
|
|
121
|
+
"'memory'. With no --run-id one is generated and printed.",
|
|
122
|
+
)
|
|
123
|
+
parser.add_argument(
|
|
124
|
+
"--run-id", metavar="ID", default=None,
|
|
125
|
+
help="Opaque key for the durable run (the checkpoint record key). "
|
|
126
|
+
"Required to --resume; auto-generated for a fresh --checkpoint run.",
|
|
127
|
+
)
|
|
128
|
+
parser.add_argument(
|
|
129
|
+
"--resume", action="store_true",
|
|
130
|
+
help="Continue an existing run from its checkpoint (needs --checkpoint + --run-id).",
|
|
131
|
+
)
|
|
132
|
+
parser.add_argument(
|
|
133
|
+
"--human-input", metavar="TEXT", default=None,
|
|
134
|
+
help="On --resume, the human's answer as a string.",
|
|
135
|
+
)
|
|
136
|
+
parser.add_argument(
|
|
137
|
+
"--human-input-json", metavar="JSON", default=None,
|
|
138
|
+
help="On --resume, the human's answer parsed as a JSON value (e.g. "
|
|
139
|
+
"'{\"approved\": true}').",
|
|
140
|
+
)
|
|
141
|
+
parser.add_argument(
|
|
142
|
+
"--human-input-file", metavar="PATH", default=None,
|
|
143
|
+
help="On --resume, read the human's answer from a file (large / multiline).",
|
|
144
|
+
)
|
|
145
|
+
args = parser.parse_args(argv)
|
|
146
|
+
|
|
147
|
+
for module in args.tools:
|
|
148
|
+
importlib.import_module(module)
|
|
149
|
+
|
|
150
|
+
# --log-level routes events through standard logging; otherwise use the console printer.
|
|
151
|
+
tracer = console_tracer()
|
|
152
|
+
if args.log_level:
|
|
153
|
+
level = getattr(logging, args.log_level.upper(), None)
|
|
154
|
+
if not isinstance(level, int):
|
|
155
|
+
parser.error(f"invalid --log-level {args.log_level!r} (use DEBUG/INFO/WARNING/ERROR)")
|
|
156
|
+
configure_logging(level)
|
|
157
|
+
tracer = event_logger()
|
|
158
|
+
|
|
159
|
+
orch = Orchestrator.from_yaml(
|
|
160
|
+
args.pipeline, registry, on_event=None if args.dry_run else tracer
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
if args.dry_run:
|
|
164
|
+
print(f"pipeline: {args.pipeline}")
|
|
165
|
+
for name, agent in orch.agents.items():
|
|
166
|
+
print(
|
|
167
|
+
f" agent {name}: provider={agent.provider.name} model={agent.model} "
|
|
168
|
+
f"tools={[t.name for t in agent.tools]}"
|
|
169
|
+
)
|
|
170
|
+
print(render_graph(orch.graph))
|
|
171
|
+
return 0
|
|
172
|
+
|
|
173
|
+
# --- RESUME: continue a checkpointed run; _run_to_exit maps the outcome ---
|
|
174
|
+
if args.resume:
|
|
175
|
+
if not args.checkpoint or not args.run_id:
|
|
176
|
+
parser.error("--resume requires --checkpoint and --run-id")
|
|
177
|
+
human = _resolve_human_input(args, parser) # at most one --human-input*; None if absent
|
|
178
|
+
cp = get_checkpointer(args.checkpoint)
|
|
179
|
+
|
|
180
|
+
def _resume() -> object:
|
|
181
|
+
nonlocal human
|
|
182
|
+
while True:
|
|
183
|
+
try:
|
|
184
|
+
return orch.resume(args.run_id, cp, human_input=human)
|
|
185
|
+
except HumanInputRequired as need:
|
|
186
|
+
# pending pause, no answer given. Non-interactive: re-raise so
|
|
187
|
+
# _run_to_exit prints how to resume and exits 2. On a TTY: prompt
|
|
188
|
+
# and re-resume with the typed answer. A *subsequent* gate raises
|
|
189
|
+
# NodePaused → exit 2 (distinct pauses loop across invocations,
|
|
190
|
+
# not within one process — the stateless resume model).
|
|
191
|
+
if not sys.stdin.isatty():
|
|
192
|
+
raise
|
|
193
|
+
print(need.prompt)
|
|
194
|
+
human = input("> ")
|
|
195
|
+
|
|
196
|
+
return _run_to_exit(
|
|
197
|
+
_resume, pipeline=args.pipeline, spec=args.checkpoint, run_id=args.run_id
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
# A human: gate pauses the run; with no durable checkpoint the pause cannot be
|
|
201
|
+
# resumed (and a printed resume command would name a throwaway in-memory sink).
|
|
202
|
+
# Fail fast with guidance rather than pausing into an unrecoverable state.
|
|
203
|
+
if not args.checkpoint and _has_human_node(orch.graph):
|
|
204
|
+
parser.error(
|
|
205
|
+
"this pipeline has a human: gate and needs --checkpoint to be resumable "
|
|
206
|
+
"(e.g. --checkpoint memory, or --checkpoint json:./runs for a durable run)"
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
initial = {}
|
|
210
|
+
for item in args.set:
|
|
211
|
+
key, _, value = item.partition("=")
|
|
212
|
+
initial[key] = value
|
|
213
|
+
|
|
214
|
+
# --- RUN with a checkpointer: durable run (pause → exit 2, completion → exit 0) ---
|
|
215
|
+
if args.checkpoint:
|
|
216
|
+
cp = get_checkpointer(args.checkpoint)
|
|
217
|
+
run_id = args.run_id or uuid.uuid4().hex
|
|
218
|
+
if not args.run_id:
|
|
219
|
+
print(f"run-id: {run_id}") # generated — print it so the user can resume
|
|
220
|
+
return _run_to_exit(
|
|
221
|
+
lambda: orch.run(initial, run_id=run_id, checkpointer=cp),
|
|
222
|
+
pipeline=args.pipeline, spec=args.checkpoint, run_id=run_id,
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
# --- RUN with no checkpointer: no I/O, run_id unnecessary ---
|
|
226
|
+
return _run_to_exit(
|
|
227
|
+
lambda: orch.run(initial), pipeline=args.pipeline, spec=None, run_id=None
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
if __name__ == "__main__":
|
|
232
|
+
sys.exit(main())
|
agentic_flow/console.py
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"""The built-in console sink for the ``on_event`` seam.
|
|
2
|
+
|
|
3
|
+
A run reports progress through a single ``on_event(event, data)`` callback. This
|
|
4
|
+
module turns that stream into human-readable console lines — the **one** place the
|
|
5
|
+
event taxonomy is rendered for a terminal, shared by the CLI (``agentic-flow …``)
|
|
6
|
+
and every example's ``run.py``. Its sibling sink, :func:`agentic_flow.event_logger`,
|
|
7
|
+
routes the same events through Python :mod:`logging` (with per-event severity
|
|
8
|
+
instead of layout); both trim long values through :func:`clip`.
|
|
9
|
+
|
|
10
|
+
The event taxonomy this renders (the ``data`` keys each event carries — the graph
|
|
11
|
+
runtime's :func:`agentic_flow.runtime._emit` calls are the source of truth):
|
|
12
|
+
|
|
13
|
+
* ``node_start`` — ``node`` + ``kind`` (the node's body kind: agent/tool/human/subgraph)
|
|
14
|
+
* ``node_input`` — ``node`` + ``kind`` + ``input`` (the rendered prompt str / args dict; not rendered here — layer-2 tracing)
|
|
15
|
+
* ``node_end`` — ``node`` + ``output``
|
|
16
|
+
* ``node_error`` — ``node`` + ``error`` (exception class) + ``message`` (not rendered here — layer-2 tracing)
|
|
17
|
+
* ``checkpoint`` — ``run_id`` + ``frontier`` (the ready node ids saved at this boundary)
|
|
18
|
+
* ``human`` — ``node`` + ``output`` [+ ``prompt`` (pause) | ``resumed`` (resume)]
|
|
19
|
+
* ``route`` — ``src`` + ``dst`` + ``seq`` (a fresh model-driven authority-L1 route choice)
|
|
20
|
+
* ``replan`` — ``replans_used`` (an accepted authority-L2 graph amendment)
|
|
21
|
+
* ``replan_rejected`` — ``gate`` + ``message`` (the validation gate the planner's batch failed)
|
|
22
|
+
* ``tool_call`` — ``agent`` + ``tool`` + ``input``
|
|
23
|
+
* ``tool_result`` — ``agent`` + ``tool`` + ``result`` + ``is_error``
|
|
24
|
+
* ``messages_delivered`` — ``agent`` + ``count`` + ``todos``
|
|
25
|
+
* ``retry`` — ``label`` + ``attempt`` + ``attempts`` + ``error`` [+ ``sleep``]
|
|
26
|
+
"""
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
import json
|
|
30
|
+
from typing import Any, Callable
|
|
31
|
+
|
|
32
|
+
EventHook = Callable[[str, dict[str, Any]], None]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def clip(value: Any, limit: int = 200) -> str:
|
|
36
|
+
"""Collapse whitespace and truncate ``value`` to ``limit`` chars for a one-line trace."""
|
|
37
|
+
text = " ".join(str(value).split())
|
|
38
|
+
return text if len(text) <= limit else text[:limit] + "…"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _render(value: Any) -> str:
|
|
42
|
+
"""Pretty-print a structured (dict/list) output; leave text as-is."""
|
|
43
|
+
if isinstance(value, (dict, list)):
|
|
44
|
+
return json.dumps(value, indent=2, ensure_ascii=False)
|
|
45
|
+
return str(value)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _indent(text: str, prefix: str = " ") -> str:
|
|
49
|
+
return "\n".join(prefix + line for line in str(text).splitlines())
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def console_tracer(*, show_output: bool = False) -> EventHook:
|
|
53
|
+
"""Return an ``on_event`` hook that prints each event to stdout.
|
|
54
|
+
|
|
55
|
+
Pass ``show_output=True`` to also print each node's output value inline,
|
|
56
|
+
pretty-printing ``dict``/``list`` results (handy for examples). The CLI leaves
|
|
57
|
+
it ``False`` and prints the final state once at the end via
|
|
58
|
+
:func:`print_final_state` instead.
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
def on_event(event: str, data: dict[str, Any]) -> None:
|
|
62
|
+
if event == "node_start":
|
|
63
|
+
print(f"\n=== node '{data['node']}' ({data['kind']}) ===")
|
|
64
|
+
elif event == "checkpoint":
|
|
65
|
+
frontier = ", ".join(data.get("frontier") or []) or "(empty)"
|
|
66
|
+
print(f" ~ checkpoint {data['run_id']} | frontier: {frontier}")
|
|
67
|
+
elif event == "human":
|
|
68
|
+
prompt = f": {clip(data['prompt'])}" if data.get("prompt") else ""
|
|
69
|
+
verb = "resumed" if data.get("resumed") else "paused"
|
|
70
|
+
print(f" ? human {verb} at '{data['node']}'{prompt}")
|
|
71
|
+
elif event == "replan":
|
|
72
|
+
print(f" * replan accepted (replans used: {data['replans_used']})")
|
|
73
|
+
elif event == "replan_rejected":
|
|
74
|
+
print(f" * replan rejected at gate '{data['gate']}': {clip(data['message'])}")
|
|
75
|
+
elif event == "route":
|
|
76
|
+
print(f" > route {data['src']} → {data['dst']}")
|
|
77
|
+
elif event == "tool_call":
|
|
78
|
+
print(f" [{data['agent']}] -> {data['tool']}({clip(data['input'])})")
|
|
79
|
+
elif event == "tool_result":
|
|
80
|
+
tag = "ERROR" if data["is_error"] else "ok"
|
|
81
|
+
print(f" [{data['agent']}] <- {data['tool']} [{tag}]: {clip(data['result'])}")
|
|
82
|
+
elif event == "messages_delivered":
|
|
83
|
+
bits = []
|
|
84
|
+
if data.get("count"):
|
|
85
|
+
bits.append(f"{data['count']} message(s)")
|
|
86
|
+
if data.get("todos"):
|
|
87
|
+
bits.append(f"{data['todos']} todo(s)")
|
|
88
|
+
print(f" [{data['agent']}] inbox: {', '.join(bits) or 'nothing'} delivered")
|
|
89
|
+
elif event == "retry":
|
|
90
|
+
sleep = f"; sleeping {data['sleep']}s" if data.get("sleep") is not None else ""
|
|
91
|
+
print(
|
|
92
|
+
f" ! retry {data['label']} ({data['attempt']}/{data['attempts']}) "
|
|
93
|
+
f"after {data['error']}{sleep}"
|
|
94
|
+
)
|
|
95
|
+
elif event == "node_end":
|
|
96
|
+
if show_output:
|
|
97
|
+
print(f" = node {data['node']!r} done:\n{_indent(_render(data['output']))}")
|
|
98
|
+
else:
|
|
99
|
+
print(f" = node {data['node']!r} done")
|
|
100
|
+
|
|
101
|
+
return on_event
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def print_final_state(state: Any) -> None:
|
|
105
|
+
"""Print the final shared state: store keys, the message log, and the todo board."""
|
|
106
|
+
print("\n=== final state ===")
|
|
107
|
+
for key in state:
|
|
108
|
+
print(f" {key}: {clip(state[key])}")
|
|
109
|
+
if state.log:
|
|
110
|
+
print(f" messages: {state.log}")
|
|
111
|
+
if state.todos:
|
|
112
|
+
print(" todos:")
|
|
113
|
+
for t in state.todos:
|
|
114
|
+
print(f" [{t['status']:>11}] #{t['id']} ({t['owner'] or '-'}) {t['text']}")
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
"""The core engine: the self-sufficient graph executor (layers 0+1).
|
|
2
|
+
|
|
3
|
+
Given a ``Graph``, a ``Store``, agents, and tools, ``core.runtime.run(...)``
|
|
4
|
+
executes and checkpoints with no layer-2 module imported. Imports nothing
|
|
5
|
+
upward (ADR 0004) — the one-way boundary enforced by ``tests/test_layering.py``.
|
|
6
|
+
"""
|