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.
- agentino/__init__.py +175 -0
- agentino/__main__.py +502 -0
- agentino/builtin_tools.py +561 -0
- agentino/cli/__init__.py +0 -0
- agentino/cli/json_emitter.py +130 -0
- agentino/cli/renderer.py +390 -0
- agentino/config/__init__.py +182 -0
- agentino/config/agents_yaml.py +399 -0
- agentino/config/pipeline_yaml.py +95 -0
- agentino/config/tools_yaml.py +192 -0
- agentino/config/utils.py +49 -0
- agentino/core/__init__.py +0 -0
- agentino/core/agent.py +738 -0
- agentino/core/context.py +58 -0
- agentino/core/extensions.py +226 -0
- agentino/core/llm.py +589 -0
- agentino/core/message.py +177 -0
- agentino/core/models.py +203 -0
- agentino/core/runner.py +549 -0
- agentino/core/session.py +109 -0
- agentino/core/state.py +120 -0
- agentino/core/tool.py +317 -0
- agentino/extras/__init__.py +0 -0
- agentino/extras/audio.py +221 -0
- agentino/extras/audit.py +109 -0
- agentino/extras/knowledge.py +641 -0
- agentino/extras/memory.py +173 -0
- agentino/extras/skills.py +152 -0
- agentino/extras/usage.py +243 -0
- agentino/pipeline/__init__.py +27 -0
- agentino/pipeline/core.py +179 -0
- agentino/pipeline/staged.py +598 -0
- agentino/providers/__init__.py +0 -0
- agentino/providers/anthropic.py +112 -0
- agentino/providers/codex.py +228 -0
- agentino/py.typed +0 -0
- agentino/reliability/__init__.py +0 -0
- agentino/reliability/compaction.py +147 -0
- agentino/reliability/errors.py +312 -0
- agentino/reliability/resilience.py +242 -0
- agentino/safety/__init__.py +0 -0
- agentino/safety/auth.py +403 -0
- agentino/safety/gates.py +86 -0
- agentino/safety/hooks.py +256 -0
- agentino/safety/sanitize.py +95 -0
- agentino/safety/security.py +152 -0
- agentino/scheduler/README.md +63 -0
- agentino/scheduler/__init__.py +71 -0
- agentino/scheduler/core.py +430 -0
- agentino/scheduler/delivery.py +100 -0
- agentino/scheduler/executor.py +68 -0
- agentino/scheduler/file_store.py +183 -0
- agentino/scheduler/picker.py +51 -0
- agentino/scheduler/sqlite_store.py +112 -0
- agentino/scheduler/store.py +84 -0
- agentino/tools/__init__.py +6 -0
- agentino/tools/std/__init__.py +108 -0
- agentino/tools/std/_agent_memory.py +423 -0
- agentino/tools/std/_file_storage.py +160 -0
- agentino/tools/std/_llm_env.py +32 -0
- agentino/tools/std/_pdf.py +107 -0
- agentino/tools/std/_weather.py +264 -0
- agentino/tools/std/_web_search.py +17 -0
- agentino/tools/std/create_csv.py +54 -0
- agentino/tools/std/create_document.py +236 -0
- agentino/tools/std/create_pdf.py +180 -0
- agentino/tools/std/create_presentation.py +141 -0
- agentino/tools/std/create_spreadsheet.py +135 -0
- agentino/tools/std/fetch_web_data.py +201 -0
- agentino/tools/std/forget.py +25 -0
- agentino/tools/std/get_weather.py +69 -0
- agentino/tools/std/get_weather_forecast.py +71 -0
- agentino/tools/std/list_files.py +52 -0
- agentino/tools/std/read_file.py +217 -0
- agentino/tools/std/read_memory.py +34 -0
- agentino/tools/std/read_rss.py +68 -0
- agentino/tools/std/remember.py +42 -0
- agentino/tools/std/storage.py +145 -0
- agentino/tools/std/translate_text.py +178 -0
- agentino/tools/std/update_memory.py +30 -0
- agentino/transport/__init__.py +21 -0
- agentino/transport/channel.py +236 -0
- agentino/transport/gateway.py +291 -0
- agentino/transport/slack.py +264 -0
- agentino/transport/telegram.py +267 -0
- agentino/transport/webhook.py +77 -0
- agentino/transport/websocket.py +360 -0
- agentino/transport/whatsapp-bridge/bridge.js +306 -0
- agentino/transport/whatsapp-bridge/package.json +17 -0
- agentino/transport/whatsapp.py +188 -0
- agentino/workers/__init__.py +18 -0
- agentino/workers/coordinator.py +320 -0
- agentino/workers/fork.py +204 -0
- agentino/workers/spawn.py +222 -0
- agentino_framework-1.1.0.dist-info/METADATA +327 -0
- agentino_framework-1.1.0.dist-info/RECORD +100 -0
- agentino_framework-1.1.0.dist-info/WHEEL +4 -0
- agentino_framework-1.1.0.dist-info/entry_points.txt +2 -0
- agentino_framework-1.1.0.dist-info/licenses/LICENSE +202 -0
- 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()
|