agentino-framework 1.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.
Files changed (100) hide show
  1. agentino/__init__.py +175 -0
  2. agentino/__main__.py +502 -0
  3. agentino/builtin_tools.py +561 -0
  4. agentino/cli/__init__.py +0 -0
  5. agentino/cli/json_emitter.py +130 -0
  6. agentino/cli/renderer.py +390 -0
  7. agentino/config/__init__.py +182 -0
  8. agentino/config/agents_yaml.py +399 -0
  9. agentino/config/pipeline_yaml.py +95 -0
  10. agentino/config/tools_yaml.py +192 -0
  11. agentino/config/utils.py +49 -0
  12. agentino/core/__init__.py +0 -0
  13. agentino/core/agent.py +738 -0
  14. agentino/core/context.py +58 -0
  15. agentino/core/extensions.py +226 -0
  16. agentino/core/llm.py +589 -0
  17. agentino/core/message.py +177 -0
  18. agentino/core/models.py +203 -0
  19. agentino/core/runner.py +549 -0
  20. agentino/core/session.py +109 -0
  21. agentino/core/state.py +120 -0
  22. agentino/core/tool.py +317 -0
  23. agentino/extras/__init__.py +0 -0
  24. agentino/extras/audio.py +221 -0
  25. agentino/extras/audit.py +109 -0
  26. agentino/extras/knowledge.py +641 -0
  27. agentino/extras/memory.py +173 -0
  28. agentino/extras/skills.py +152 -0
  29. agentino/extras/usage.py +243 -0
  30. agentino/pipeline/__init__.py +27 -0
  31. agentino/pipeline/core.py +179 -0
  32. agentino/pipeline/staged.py +598 -0
  33. agentino/providers/__init__.py +0 -0
  34. agentino/providers/anthropic.py +112 -0
  35. agentino/providers/codex.py +228 -0
  36. agentino/py.typed +0 -0
  37. agentino/reliability/__init__.py +0 -0
  38. agentino/reliability/compaction.py +147 -0
  39. agentino/reliability/errors.py +312 -0
  40. agentino/reliability/resilience.py +242 -0
  41. agentino/safety/__init__.py +0 -0
  42. agentino/safety/auth.py +403 -0
  43. agentino/safety/gates.py +86 -0
  44. agentino/safety/hooks.py +256 -0
  45. agentino/safety/sanitize.py +95 -0
  46. agentino/safety/security.py +152 -0
  47. agentino/scheduler/README.md +63 -0
  48. agentino/scheduler/__init__.py +71 -0
  49. agentino/scheduler/core.py +430 -0
  50. agentino/scheduler/delivery.py +100 -0
  51. agentino/scheduler/executor.py +68 -0
  52. agentino/scheduler/file_store.py +183 -0
  53. agentino/scheduler/picker.py +51 -0
  54. agentino/scheduler/sqlite_store.py +112 -0
  55. agentino/scheduler/store.py +84 -0
  56. agentino/tools/__init__.py +6 -0
  57. agentino/tools/std/__init__.py +108 -0
  58. agentino/tools/std/_agent_memory.py +423 -0
  59. agentino/tools/std/_file_storage.py +160 -0
  60. agentino/tools/std/_llm_env.py +32 -0
  61. agentino/tools/std/_pdf.py +107 -0
  62. agentino/tools/std/_weather.py +264 -0
  63. agentino/tools/std/_web_search.py +17 -0
  64. agentino/tools/std/create_csv.py +54 -0
  65. agentino/tools/std/create_document.py +236 -0
  66. agentino/tools/std/create_pdf.py +180 -0
  67. agentino/tools/std/create_presentation.py +141 -0
  68. agentino/tools/std/create_spreadsheet.py +135 -0
  69. agentino/tools/std/fetch_web_data.py +201 -0
  70. agentino/tools/std/forget.py +25 -0
  71. agentino/tools/std/get_weather.py +69 -0
  72. agentino/tools/std/get_weather_forecast.py +71 -0
  73. agentino/tools/std/list_files.py +52 -0
  74. agentino/tools/std/read_file.py +217 -0
  75. agentino/tools/std/read_memory.py +34 -0
  76. agentino/tools/std/read_rss.py +68 -0
  77. agentino/tools/std/remember.py +42 -0
  78. agentino/tools/std/storage.py +145 -0
  79. agentino/tools/std/translate_text.py +178 -0
  80. agentino/tools/std/update_memory.py +30 -0
  81. agentino/transport/__init__.py +21 -0
  82. agentino/transport/channel.py +236 -0
  83. agentino/transport/gateway.py +291 -0
  84. agentino/transport/slack.py +264 -0
  85. agentino/transport/telegram.py +267 -0
  86. agentino/transport/webhook.py +77 -0
  87. agentino/transport/websocket.py +360 -0
  88. agentino/transport/whatsapp-bridge/bridge.js +306 -0
  89. agentino/transport/whatsapp-bridge/package.json +17 -0
  90. agentino/transport/whatsapp.py +188 -0
  91. agentino/workers/__init__.py +18 -0
  92. agentino/workers/coordinator.py +320 -0
  93. agentino/workers/fork.py +204 -0
  94. agentino/workers/spawn.py +222 -0
  95. agentino_framework-1.1.0.dist-info/METADATA +327 -0
  96. agentino_framework-1.1.0.dist-info/RECORD +100 -0
  97. agentino_framework-1.1.0.dist-info/WHEEL +4 -0
  98. agentino_framework-1.1.0.dist-info/entry_points.txt +2 -0
  99. agentino_framework-1.1.0.dist-info/licenses/LICENSE +202 -0
  100. agentino_framework-1.1.0.dist-info/licenses/NOTICE +16 -0
agentino/__init__.py ADDED
@@ -0,0 +1,175 @@
1
+ """Agentino — lightweight Python agent framework.
2
+
3
+ Config → agents → run. That's it.
4
+
5
+ agentino run agents.yml # REPL
6
+ agentino run agents.yml --agent reviewer # specific agent
7
+ agentino run agents.yml --serve 8080 # HTTP server
8
+ agentino run agents.yml -m "Review PR #42" # one-shot
9
+ """
10
+
11
+ from agentino.builtin_tools import BUILTIN_TOOLS
12
+ from agentino.cli.renderer import CLIRenderer
13
+ from agentino.config import Config, load_agents, load_config
14
+ from agentino.core.agent import Agent
15
+ from agentino.core.extensions import ExtensionLoader, ReloadResult
16
+ from agentino.core.message import Attachment, Event, EventType, Message, ToolCall, Usage
17
+ from agentino.core.models import (
18
+ ModelCost,
19
+ ModelInfo,
20
+ all_models,
21
+ lookup_model,
22
+ register_model,
23
+ )
24
+ from agentino.core.runner import Runner, create_runner
25
+ from agentino.core.session import Session
26
+ from agentino.core.state import (
27
+ get_session_id,
28
+ get_state,
29
+ record_model_usage,
30
+ record_skill,
31
+ reset_state,
32
+ )
33
+ from agentino.core.tool import FinalResult, Tool, tool
34
+ from agentino.extras.audio import AudioTranscriber, build_transcriber
35
+ from agentino.extras.audit import AuditLog
36
+ from agentino.extras.knowledge import KnowledgeBase
37
+ from agentino.extras.memory import MemoryEntry, MemoryStore
38
+ from agentino.extras.skills import SkillMeta, SkillRegistry
39
+ from agentino.extras.usage import UsageTracker
40
+ from agentino.pipeline.core import ParallelPipeline, Pipeline, RouterPipeline, Step
41
+ from agentino.pipeline.staged import (
42
+ FactStore,
43
+ StageDef,
44
+ StagedPipeline,
45
+ StageResult,
46
+ judge_stage_failure,
47
+ parse_verdict,
48
+ summarize_stage_output,
49
+ )
50
+ from agentino.reliability.errors import (
51
+ ErrorClass,
52
+ ToolError,
53
+ classify_error,
54
+ error_blocked,
55
+ error_duplicate,
56
+ error_internal,
57
+ error_invalid_args,
58
+ error_not_found,
59
+ error_permission,
60
+ error_timeout,
61
+ error_unavailable,
62
+ error_unknown_tool,
63
+ error_validation,
64
+ format_error,
65
+ get_overflow_tokens,
66
+ get_retry_delay,
67
+ get_ssl_hint,
68
+ )
69
+ from agentino.reliability.resilience import (
70
+ compact_history,
71
+ estimate_tokens,
72
+ repair_messages,
73
+ retry_with_backoff,
74
+ strip_think_tags,
75
+ truncate_result,
76
+ )
77
+ from agentino.safety.gates import GateManager, GateRule
78
+ from agentino.safety.hooks import HOOK_EVENTS, HookManager, HookResult
79
+ from agentino.safety.sanitize import clean_path, normalize_text, sanitize_tool_args
80
+ from agentino.safety.security import INJECTION_PATTERNS, check_security, make_security_scan_tool
81
+ from agentino.workers import make_spawn_tool
82
+
83
+ from .core import context
84
+
85
+ __version__ = "1.1.0"
86
+
87
+
88
+ __all__ = [
89
+ "Agent",
90
+ "Attachment",
91
+ "AudioTranscriber",
92
+ "AuditLog",
93
+ "BUILTIN_TOOLS",
94
+ "CLIRenderer",
95
+ "Config",
96
+ "ErrorClass",
97
+ "Event",
98
+ "EventType",
99
+ "ExtensionLoader",
100
+ "FactStore",
101
+ "FinalResult",
102
+ "GateManager",
103
+ "GateRule",
104
+ "HOOK_EVENTS",
105
+ "HookManager",
106
+ "HookResult",
107
+ "INJECTION_PATTERNS",
108
+ "KnowledgeBase",
109
+ "MemoryEntry",
110
+ "MemoryStore",
111
+ "Message",
112
+ "ModelCost",
113
+ "ModelInfo",
114
+ "ParallelPipeline",
115
+ "Pipeline",
116
+ "ReloadResult",
117
+ "RouterPipeline",
118
+ "Runner",
119
+ "Session",
120
+ "SkillMeta",
121
+ "SkillRegistry",
122
+ "StageDef",
123
+ "StageResult",
124
+ "StagedPipeline",
125
+ "Step",
126
+ "Tool",
127
+ "ToolCall",
128
+ "ToolError",
129
+ "Usage",
130
+ "UsageTracker",
131
+ "all_models",
132
+ "build_transcriber",
133
+ "check_security",
134
+ "classify_error",
135
+ "clean_path",
136
+ "compact_history",
137
+ "context",
138
+ "create_runner",
139
+ "error_blocked",
140
+ "error_duplicate",
141
+ "error_internal",
142
+ "error_invalid_args",
143
+ "error_not_found",
144
+ "error_permission",
145
+ "error_timeout",
146
+ "error_unavailable",
147
+ "error_unknown_tool",
148
+ "error_validation",
149
+ "estimate_tokens",
150
+ "format_error",
151
+ "get_overflow_tokens",
152
+ "get_retry_delay",
153
+ "get_session_id",
154
+ "get_ssl_hint",
155
+ "get_state",
156
+ "judge_stage_failure",
157
+ "load_agents",
158
+ "load_config",
159
+ "lookup_model",
160
+ "make_security_scan_tool",
161
+ "make_spawn_tool",
162
+ "normalize_text",
163
+ "parse_verdict",
164
+ "record_model_usage",
165
+ "record_skill",
166
+ "register_model",
167
+ "repair_messages",
168
+ "reset_state",
169
+ "retry_with_backoff",
170
+ "sanitize_tool_args",
171
+ "strip_think_tags",
172
+ "summarize_stage_output",
173
+ "tool",
174
+ "truncate_result",
175
+ ]
agentino/__main__.py ADDED
@@ -0,0 +1,502 @@
1
+ """CLI entry point — python -m agentino, or `agentino` command.
2
+
3
+ Usage:
4
+ agentino run agents.yml # REPL with default agent
5
+ agentino run agents.yml --agent reviewer # REPL with specific agent
6
+ agentino run agents.yml --message "hello" # one-shot
7
+ agentino run agents.yml --serve 8080 # HTTP server
8
+ agentino chat # quick REPL, no config
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import argparse
14
+ import sys
15
+ from pathlib import Path
16
+
17
+
18
+ def main() -> None:
19
+ """CLI entry point for the agentino command."""
20
+ parser = argparse.ArgumentParser(
21
+ prog="agentino",
22
+ description="Agentino — lightweight Python agent framework",
23
+ )
24
+ sub = parser.add_subparsers(dest="command")
25
+
26
+ # --- run ---
27
+ run_parser = sub.add_parser("run", help="Run agents from a config file")
28
+ run_parser.add_argument(
29
+ "config",
30
+ nargs="?",
31
+ default=None,
32
+ help="Path to agents config file (.yml, .yaml). Auto-discovers agents.yml in current directory if omitted.",
33
+ )
34
+ run_parser.add_argument(
35
+ "--agent", "-a", help="Agent name (default: first agent or default_agent)"
36
+ )
37
+ run_parser.add_argument("--message", "-m", help="One-shot message (print reply and exit)")
38
+ run_parser.add_argument("--serve", type=int, metavar="PORT", help="Start HTTP server on PORT")
39
+ run_parser.add_argument("--session-dir", default="./sessions", help="Session storage directory")
40
+ run_parser.add_argument("--usage-file", default="./usage.jsonl", help="Usage log file")
41
+ run_parser.add_argument("--session-id", default="default", help="Session ID for REPL/one-shot")
42
+ run_parser.add_argument(
43
+ "--no-session",
44
+ action="store_true",
45
+ help="Ephemeral run — don't load or persist any session history",
46
+ )
47
+ run_parser.add_argument(
48
+ "--project",
49
+ "-p",
50
+ help="Project directory (sets AGENTINO_PROJECT_DIR for file tools, coder, knowledge)",
51
+ )
52
+ run_parser.add_argument(
53
+ "--quiet", "-q", action="store_true", help="Suppress live tool call output"
54
+ )
55
+ run_parser.add_argument(
56
+ "--gateway",
57
+ action="store_true",
58
+ help="Start gateway (multi-channel: Telegram, Slack, etc.)",
59
+ )
60
+ run_parser.add_argument(
61
+ "--iterate",
62
+ "-i",
63
+ action="store_true",
64
+ help="After one-shot, drop into REPL to iterate on the result",
65
+ )
66
+ run_parser.add_argument(
67
+ "--mode",
68
+ choices=["text", "json", "jsonl"],
69
+ default="text",
70
+ help="Output mode for one-shot (--message) runs. text=ANSI-pretty (default), "
71
+ "json=single envelope at end, jsonl=streaming events + final envelope. "
72
+ "json/jsonl force --quiet to keep stdout machine-readable.",
73
+ )
74
+
75
+ # --- chat ---
76
+ chat_parser = sub.add_parser("chat", help="Quick chat REPL (no config file needed)")
77
+ chat_parser.add_argument(
78
+ "--model", "-M", default=None, help="Model to use (auto-detected from auth)"
79
+ )
80
+ chat_parser.add_argument(
81
+ "--instructions", "-i", default="You are a helpful assistant.", help="System instructions"
82
+ )
83
+ chat_parser.add_argument("--base-url", help="API base URL")
84
+ chat_parser.add_argument("--api-key", help="API key")
85
+
86
+ # --- agents ---
87
+ agents_parser = sub.add_parser("agents", help="List agents in a config file")
88
+ agents_parser.add_argument("config", help="Path to agents config file")
89
+
90
+ # --- login ---
91
+ login_parser = sub.add_parser(
92
+ "login", help="Authenticate with OpenAI (Codex subscription) or Anthropic"
93
+ )
94
+ login_parser.add_argument(
95
+ "--provider",
96
+ "-p",
97
+ choices=["openai", "anthropic"],
98
+ default="openai",
99
+ help="Provider to authenticate with (default: openai)",
100
+ )
101
+ login_parser.add_argument(
102
+ "--token", "-t", help="Paste a token directly (Anthropic setup-token or API key)"
103
+ )
104
+
105
+ # --- logout ---
106
+ sub.add_parser("logout", help="Clear stored credentials")
107
+
108
+ # --- status ---
109
+ sub.add_parser("status", help="Show auth status and stored credentials")
110
+
111
+ # --- version ---
112
+ sub.add_parser("version", help="Show version")
113
+
114
+ args = parser.parse_args()
115
+
116
+ if args.command == "version":
117
+ from . import __version__
118
+
119
+ print(f"agentino {__version__}")
120
+ return
121
+
122
+ if args.command == "login":
123
+ _cmd_login(args)
124
+ return
125
+
126
+ if args.command == "logout":
127
+ _cmd_logout()
128
+ return
129
+
130
+ if args.command == "status":
131
+ _cmd_status()
132
+ return
133
+
134
+ if args.command == "agents":
135
+ _cmd_agents(args)
136
+ return
137
+
138
+ if args.command == "chat":
139
+ _cmd_chat(args)
140
+ return
141
+
142
+ if args.command == "run":
143
+ _cmd_run(args)
144
+ return
145
+
146
+ parser.print_help()
147
+
148
+
149
+ def _cmd_login(args: argparse.Namespace) -> None:
150
+ """Authenticate with a provider."""
151
+ from agentino.safety.auth import (
152
+ AuthCredentials,
153
+ login_openai,
154
+ save_anthropic_token,
155
+ save_credentials,
156
+ )
157
+
158
+ if args.provider == "openai":
159
+ if args.token:
160
+ # Direct API key
161
+ creds = AuthCredentials(provider="openai", access_token=args.token)
162
+ save_credentials(creds)
163
+ print("Saved OpenAI API key.")
164
+ else:
165
+ # OAuth PKCE flow
166
+ print("Logging in with OpenAI (Codex subscription)...")
167
+ try:
168
+ creds = login_openai()
169
+ email = f" ({creds.email})" if creds.email else ""
170
+ print(f"Logged in to OpenAI{email}")
171
+ print(f"Token expires: {_format_expiry(creds.expires_at)}")
172
+ print("Stored in: ~/.agentino/auth.json")
173
+ except Exception as e:
174
+ print(f"Login failed: {e}", file=sys.stderr)
175
+ sys.exit(1)
176
+
177
+ elif args.provider == "anthropic":
178
+ if args.token:
179
+ save_anthropic_token(args.token)
180
+ print("Saved Anthropic token.")
181
+ else:
182
+ print("For Anthropic, run `claude setup-token` first, then:")
183
+ print(" agentino login --provider anthropic --token <your-token>")
184
+ print()
185
+ print("Or paste your API key:")
186
+ print(" agentino login --provider anthropic --token sk-ant-...")
187
+ sys.exit(1)
188
+
189
+ print("\nReady! Run: agentino run agents.yml")
190
+
191
+
192
+ def _cmd_logout() -> None:
193
+ """Clear stored credentials."""
194
+ from agentino.safety.auth import AUTH_FILE
195
+
196
+ if AUTH_FILE.exists():
197
+ AUTH_FILE.unlink()
198
+ print("Credentials cleared.")
199
+ else:
200
+ print("No stored credentials found.")
201
+
202
+
203
+ def _cmd_status() -> None:
204
+ """Show auth status."""
205
+ from agentino.safety.auth import (
206
+ AUTH_FILE,
207
+ CODEX_AUTH_FILE,
208
+ load_codex_credentials,
209
+ load_credentials,
210
+ )
211
+
212
+ print(f"Auth file: {AUTH_FILE}")
213
+ print(f"Exists: {AUTH_FILE.exists()}\n")
214
+
215
+ for provider in ("openai", "anthropic"):
216
+ creds = load_credentials(provider)
217
+ source = "agentino"
218
+
219
+ # Fall back to Codex CLI credentials for OpenAI
220
+ if not creds and provider == "openai":
221
+ creds = load_codex_credentials()
222
+ if creds:
223
+ source = "codex"
224
+
225
+ if creds:
226
+ # Mask token
227
+ token = creds.access_token
228
+ masked = token[:8] + "..." + token[-4:] if len(token) > 16 else "***"
229
+ print(f" {provider}:")
230
+ if source == "codex":
231
+ print(f" Source: Codex CLI ({CODEX_AUTH_FILE})")
232
+ print(f" Token: {masked}")
233
+ if creds.email:
234
+ print(f" Email: {creds.email}")
235
+ if creds.expires_at:
236
+ print(f" Expires: {_format_expiry(creds.expires_at)}")
237
+ if creds.is_expired:
238
+ print(" Status: EXPIRED (will auto-refresh)")
239
+ else:
240
+ print(" Status: valid")
241
+ else:
242
+ print(" Status: valid (no expiry)")
243
+ print()
244
+ else:
245
+ # Check env var
246
+ import os
247
+
248
+ env_vars = {"openai": "OPENAI_API_KEY", "anthropic": "ANTHROPIC_API_KEY"}
249
+ env_val = os.getenv(env_vars.get(provider, ""))
250
+ if env_val:
251
+ print(f" {provider}: via ${env_vars[provider]} env var")
252
+ else:
253
+ print(f" {provider}: not configured")
254
+ print()
255
+
256
+
257
+ def _format_expiry(ts: float) -> str:
258
+ from datetime import datetime
259
+
260
+ return datetime.fromtimestamp(ts).strftime("%Y-%m-%d %H:%M:%S")
261
+
262
+
263
+ def _resolve_config(config_arg: str | None) -> Path | None:
264
+ """Resolve config file path. Auto-discovers agents.yml if not specified."""
265
+ if config_arg:
266
+ return Path(config_arg)
267
+ # Auto-discover in current directory
268
+ for name in ("agents.yml", "agents.yaml"):
269
+ p = Path(name)
270
+ if p.exists():
271
+ return p
272
+ return None
273
+
274
+
275
+ def _cmd_run(args: argparse.Namespace) -> None:
276
+ """Run agents from config."""
277
+ import os
278
+
279
+ from agentino.core.runner import create_runner
280
+
281
+ config_path = _resolve_config(args.config)
282
+ if not config_path or not config_path.exists():
283
+ print(
284
+ "Error: config file not found. Provide a path or create agents.yml in the current directory."
285
+ )
286
+ sys.exit(1)
287
+
288
+ # Auto-load .env from config directory (then parent dirs)
289
+ for env_dir in [config_path.parent, config_path.parent.parent, Path.cwd()]:
290
+ env_file = env_dir / ".env"
291
+ if env_file.exists():
292
+ for line in env_file.read_text(encoding="utf-8").splitlines():
293
+ line = line.strip()
294
+ if line and not line.startswith("#") and "=" in line:
295
+ k, _, v = line.partition("=")
296
+ os.environ.setdefault(k.strip(), v.strip())
297
+ break
298
+
299
+ if args.project:
300
+ project_dir = Path(args.project).resolve()
301
+ if not project_dir.is_dir():
302
+ print(f"Error: project directory not found: {project_dir}")
303
+ sys.exit(1)
304
+ os.environ["AGENTINO_PROJECT_DIR"] = str(project_dir)
305
+
306
+ # Set config dir so tools (parallel_explore etc.) know where repos are
307
+ os.environ["AGENTINO_CONFIG_DIR"] = str(config_path.parent.resolve())
308
+
309
+ # Load tools from tools/ directory next to config if it exists
310
+ tools = _discover_tools(config_path.parent)
311
+
312
+ # json/jsonl modes write structured output to stdout — silence the
313
+ # human-readable renderer so the two streams don't collide.
314
+ machine_mode = getattr(args, "mode", "text") in ("json", "jsonl")
315
+ verbose = (not args.quiet) and not machine_mode
316
+ runner = create_runner(
317
+ config_path,
318
+ tools=tools,
319
+ session_dir=args.session_dir,
320
+ usage_file=args.usage_file,
321
+ verbose=verbose,
322
+ no_session=getattr(args, "no_session", False),
323
+ )
324
+
325
+ if args.gateway:
326
+ from agentino.transport import build_gateway
327
+
328
+ gateway = build_gateway(runner.config, session_dir=args.session_dir)
329
+ names = ", ".join(gateway.channels[i].name for i in range(len(gateway.channels)))
330
+ print(f"\n Agentino gateway — {len(gateway.channels)} channel(s): {names}")
331
+ print(" Press Ctrl+C to stop\n")
332
+ gateway.run()
333
+ return
334
+
335
+ try:
336
+ if args.serve:
337
+ runner.serve(port=args.serve)
338
+ elif args.message:
339
+ import asyncio
340
+
341
+ if machine_mode:
342
+ import contextlib
343
+ import io as _io
344
+
345
+ from agentino.cli.json_emitter import JsonEmitter
346
+
347
+ target = runner._resolve_agent(args.agent)
348
+ # Construct the emitter BEFORE redirecting stdout so it
349
+ # holds a reference to the real stdout for envelope output;
350
+ # the redirect inside the run silences any incidental
351
+ # prints from the staged-pipeline renderer / tool surface.
352
+ emitter = JsonEmitter(mode=args.mode)
353
+ prev_handler = target.on_event
354
+
355
+ def _chain(event):
356
+ emitter.handle(event)
357
+ if prev_handler:
358
+ prev_handler(event)
359
+
360
+ target.on_event = _chain
361
+ noise = _io.StringIO()
362
+ try:
363
+ with contextlib.redirect_stdout(noise):
364
+ reply = asyncio.run(runner.one_shot(args.message, agent_name=args.agent))
365
+ finally:
366
+ target.on_event = prev_handler
367
+ emitter.emit_envelope(reply, model=getattr(target, "model", "") or "")
368
+ else:
369
+ reply = asyncio.run(runner.one_shot(args.message, agent_name=args.agent))
370
+ _print_final(reply)
371
+ # --iterate: drop into REPL after one-shot to refine the result
372
+ if getattr(args, "iterate", False) and not machine_mode:
373
+ print("\n Entering iteration mode — type follow-ups to improve the document.\n")
374
+ runner.repl(agent_name=args.agent, session_id=args.session_id)
375
+ else:
376
+ runner.repl(agent_name=args.agent, session_id=args.session_id)
377
+ except Exception as e:
378
+ err = str(e)
379
+ if "401" in err or "Unauthorized" in err or "403" in err:
380
+ print(
381
+ f"\n ✗ Authentication failed. Check your API key and .env file.\n {err[:200]}\n"
382
+ )
383
+ elif "400" in err:
384
+ print(f"\n ✗ Bad request: {err[:200]}\n")
385
+ else:
386
+ print(f"\n ✗ Error: {err[:200]}\n")
387
+ sys.exit(1)
388
+
389
+
390
+ def _cmd_chat(args: argparse.Namespace) -> None:
391
+ """Quick chat without config file."""
392
+ from agentino.builtin_tools import BUILTIN_TOOLS
393
+ from agentino.core.agent import Agent
394
+ from agentino.core.session import Session
395
+ from agentino.extras.usage import UsageTracker
396
+
397
+ agent = Agent(
398
+ model=args.model, # None = auto-detect
399
+ instructions=args.instructions,
400
+ tools=BUILTIN_TOOLS,
401
+ base_url=args.base_url,
402
+ api_key=args.api_key,
403
+ )
404
+
405
+ tracker = UsageTracker()
406
+ tracker.bind(agent.model)
407
+ agent.on_event = tracker.on_event
408
+
409
+ session = Session("./sessions/chat.jsonl")
410
+
411
+ print(f"\n Agentino chat — {agent.model} via {agent._llm.provider}")
412
+ print(" Ctrl+C to quit\n")
413
+
414
+ while True:
415
+ try:
416
+ user_input = input("You: ").strip()
417
+ if not user_input:
418
+ continue
419
+ import asyncio
420
+
421
+ reply = asyncio.run(agent.run(user_input, session=session))
422
+ print(f"\nAssistant: {reply}\n")
423
+ except (KeyboardInterrupt, EOFError):
424
+ print(f"\n\n{tracker.summary()}")
425
+ print("Goodbye!")
426
+ break
427
+
428
+
429
+ def _cmd_agents(args: argparse.Namespace) -> None:
430
+ """List agents in a config file."""
431
+ from agentino.core.runner import create_runner
432
+
433
+ config_path = _resolve_config(args.config)
434
+ if not config_path or not config_path.exists():
435
+ print("Error: config file not found.")
436
+ sys.exit(1)
437
+
438
+ runner = create_runner(config_path)
439
+ for info in runner.list_agents():
440
+ tools = ", ".join(info["tools"]) if info["tools"] else "none"
441
+ print(f" {info['name']:15s} model={info['model']:20s} tools=[{tools}]")
442
+ print(f" {'':15s} {info['instructions']}")
443
+ print()
444
+
445
+
446
+ def _print_final(text: str) -> None:
447
+ """Print the final agent response, converting markdown to ANSI."""
448
+ import re
449
+
450
+ _DIM = "\033[2m"
451
+ _BOLD = "\033[1m"
452
+ _RESET = "\033[0m"
453
+
454
+ def _md_to_ansi(line: str) -> str:
455
+ # **bold** → ANSI bold
456
+ line = re.sub(r"\*\*(.+?)\*\*", rf"{_BOLD}\1{_RESET}", line)
457
+ # `code` → dim
458
+ line = re.sub(r"`(.+?)`", rf"{_DIM}\1{_RESET}", line)
459
+ # - bullet → •
460
+ line = re.sub(r"^(\s*)- ", r"\1• ", line)
461
+ # ### heading → bold
462
+ line = re.sub(r"^#{1,4}\s+(.+)", rf"{_BOLD}\1{_RESET}", line)
463
+ return line
464
+
465
+ print(f"\n{_DIM}{'─' * 60}{_RESET}")
466
+ for line in text.strip().split("\n"):
467
+ print(f" {_md_to_ansi(line)}")
468
+ print(f"{_DIM}{'─' * 60}{_RESET}\n")
469
+
470
+
471
+ def _discover_tools(directory: Path) -> list:
472
+ """Auto-discover @tool-decorated functions from tools/ directory."""
473
+ from agentino.core.tool import Tool
474
+
475
+ tools_dir = directory / "tools"
476
+ if not tools_dir.is_dir():
477
+ return []
478
+
479
+ import importlib.util
480
+
481
+ discovered: list[Tool] = []
482
+
483
+ for py_file in sorted(tools_dir.glob("*.py")):
484
+ if py_file.name.startswith("_"):
485
+ continue
486
+ try:
487
+ spec = importlib.util.spec_from_file_location(py_file.stem, py_file)
488
+ if spec and spec.loader:
489
+ module = importlib.util.module_from_spec(spec)
490
+ spec.loader.exec_module(module)
491
+ for attr_name in dir(module):
492
+ attr = getattr(module, attr_name)
493
+ if isinstance(attr, Tool):
494
+ discovered.append(attr)
495
+ except Exception as e:
496
+ print(f"Warning: failed to load {py_file}: {e}", file=sys.stderr)
497
+
498
+ return discovered
499
+
500
+
501
+ if __name__ == "__main__":
502
+ main()