nexusagent-runtime 0.4.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.
nexusagent/__init__.py ADDED
@@ -0,0 +1,15 @@
1
+ from nexusagent.agent import Agent, AgentResult
2
+ from nexusagent.application import create_application_runtime
3
+ from nexusagent.config import Settings
4
+ from nexusagent.runtime import Runtime, create_runtime
5
+
6
+ __version__ = "0.4.0"
7
+
8
+ __all__ = [
9
+ "Agent",
10
+ "AgentResult",
11
+ "Runtime",
12
+ "Settings",
13
+ "create_application_runtime",
14
+ "create_runtime",
15
+ ]
nexusagent/__main__.py ADDED
@@ -0,0 +1,3 @@
1
+ from nexusagent.main import main
2
+
3
+ main()
nexusagent/agent.py ADDED
@@ -0,0 +1,175 @@
1
+ """Agent Core for NexusAgent.
2
+
3
+ Defines the contract for running an agent: input text in, an AgentResult out.
4
+ The Agent delegates generation to a Provider. When it is given a ToolRegistry
5
+ and a ToolExecutor it also runs the autonomous tool loop:
6
+
7
+ provider responds -> if it asks for a tool, run it -> feed the result back
8
+ -> provider responds again -> ... until it answers in plain text
9
+
10
+ The loop is bounded by ``max_iterations`` so a misbehaving backend can never
11
+ keep the agent running forever. Explicit ``execute_tool()`` remains available.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ from dataclasses import dataclass
17
+
18
+ from nexusagent.logging import get_logger
19
+ from nexusagent.provider import (
20
+ Provider,
21
+ ProviderResponse,
22
+ TextResponse,
23
+ ToolCallRequest,
24
+ ToolResult,
25
+ ToolSpec,
26
+ )
27
+ from nexusagent.tool_executor import ToolExecutionError, ToolExecutor
28
+ from nexusagent.tool_registry import ToolRegistry
29
+
30
+ DEFAULT_MAX_ITERATIONS = 10
31
+
32
+ _logger = get_logger("nexusagent.agent")
33
+
34
+
35
+ @dataclass(frozen=True)
36
+ class AgentResult:
37
+ """The result of a single agent run."""
38
+
39
+ output: str
40
+ success: bool
41
+
42
+
43
+ class Agent:
44
+ def __init__(
45
+ self,
46
+ provider: Provider,
47
+ tool_registry: ToolRegistry | None = None,
48
+ tool_executor: ToolExecutor | None = None,
49
+ max_iterations: int = DEFAULT_MAX_ITERATIONS,
50
+ ) -> None:
51
+ if isinstance(max_iterations, bool) or not isinstance(max_iterations, int):
52
+ raise TypeError("max_iterations must be an integer")
53
+
54
+ if max_iterations < 1:
55
+ raise ValueError("max_iterations must be at least 1")
56
+
57
+ self.provider = provider
58
+ self.tool_registry = tool_registry
59
+ self.tool_executor = tool_executor
60
+ self.max_iterations = max_iterations
61
+
62
+ def run(self, input_text: str) -> AgentResult:
63
+ """Run the agent on the given input and return an AgentResult.
64
+
65
+ If the Provider requests tools and this Agent has a ToolRegistry and a
66
+ ToolExecutor, the requested tools are executed and their results are
67
+ fed back to the Provider until it returns a plain text answer or
68
+ ``max_iterations`` provider calls have been made.
69
+ """
70
+ if not isinstance(input_text, str):
71
+ raise TypeError("input_text must be a string")
72
+
73
+ if not input_text.strip():
74
+ raise ValueError("input_text must not be empty or whitespace-only")
75
+
76
+ can_use_tools = self.tool_registry is not None and self.tool_executor is not None
77
+ tools = self._tool_specs() if can_use_tools else ()
78
+ tool_results: list[ToolResult] = []
79
+
80
+ for iteration in range(1, self.max_iterations + 1):
81
+ try:
82
+ response = self._ask_provider(input_text, tools, tuple(tool_results))
83
+ except Exception as exc: # noqa: BLE001
84
+ return AgentResult(output=f"provider error: {exc}", success=False)
85
+
86
+ if isinstance(response, TextResponse):
87
+ _logger.debug("iteration %d: provider answered with text", iteration)
88
+ return AgentResult(output=response.text, success=True)
89
+
90
+ if not isinstance(response, ToolCallRequest):
91
+ return AgentResult(
92
+ output=f"provider error: unexpected response type {type(response).__name__}",
93
+ success=False,
94
+ )
95
+
96
+ if not can_use_tools:
97
+ return AgentResult(
98
+ output=(
99
+ f"provider requested tool {response.tool_name!r} "
100
+ "but the agent has no tools configured"
101
+ ),
102
+ success=False,
103
+ )
104
+
105
+ _logger.info("iteration %d: provider requested tool %r", iteration, response.tool_name)
106
+ tool_results.append(self._run_tool_call(response))
107
+
108
+ return AgentResult(
109
+ output=f"agent stopped: exceeded the maximum of {self.max_iterations} iterations",
110
+ success=False,
111
+ )
112
+
113
+ def execute_tool(self, tool_name: str, input_data: str) -> str:
114
+ """Execute a specific, explicitly named tool and return its output."""
115
+ if self.tool_registry is None:
116
+ raise RuntimeError("Agent requires a ToolRegistry to execute tools")
117
+
118
+ if self.tool_executor is None:
119
+ raise RuntimeError("Agent requires a ToolExecutor to execute tools")
120
+
121
+ tool = self.tool_registry.get(tool_name)
122
+ return self.tool_executor.execute(tool, input_data)
123
+
124
+ def has_tool(self, tool_name: str) -> bool:
125
+ """Return whether a tool is currently available through the Agent."""
126
+ if self.tool_registry is None:
127
+ raise RuntimeError("Agent requires a ToolRegistry to check tool availability")
128
+
129
+ return self.tool_registry.has(tool_name)
130
+
131
+ def _ask_provider(
132
+ self,
133
+ input_text: str,
134
+ tools: tuple[ToolSpec, ...],
135
+ tool_results: tuple[ToolResult, ...],
136
+ ) -> ProviderResponse:
137
+ """Ask the provider for its next response.
138
+
139
+ Any object with a ``generate(prompt) -> str`` method is accepted as a
140
+ provider; only ones that also define ``generate_response`` can request
141
+ tools.
142
+ """
143
+ generate_response = getattr(self.provider, "generate_response", None)
144
+ if generate_response is None:
145
+ return TextResponse(self.provider.generate(input_text))
146
+
147
+ response: ProviderResponse = generate_response(
148
+ input_text, tools=tools, tool_results=tool_results
149
+ )
150
+ return response
151
+
152
+ def _tool_specs(self) -> tuple[ToolSpec, ...]:
153
+ assert self.tool_registry is not None
154
+ return tuple(ToolSpec(t.name, t.description) for t in self.tool_registry.list_tools())
155
+
156
+ def _run_tool_call(self, request: ToolCallRequest) -> ToolResult:
157
+ """Run one requested tool. Failures become error results, never crashes.
158
+
159
+ The Provider (usually a model) is told what went wrong so it can retry
160
+ with better input, pick another tool, or explain the problem.
161
+ """
162
+ try:
163
+ output = self.execute_tool(request.tool_name, request.tool_input)
164
+ except KeyError:
165
+ return self._error_result(request, f"Tool {request.tool_name!r} is not registered")
166
+ except (TypeError, ValueError, ToolExecutionError) as exc:
167
+ return self._error_result(request, str(exc))
168
+
169
+ _logger.debug("tool %r succeeded", request.tool_name)
170
+ return ToolResult(request.tool_name, request.tool_input, output)
171
+
172
+ @staticmethod
173
+ def _error_result(request: ToolCallRequest, message: str) -> ToolResult:
174
+ _logger.warning("tool %r failed: %s", request.tool_name, message)
175
+ return ToolResult(request.tool_name, request.tool_input, message, is_error=True)
@@ -0,0 +1,11 @@
1
+ from __future__ import annotations
2
+
3
+ from nexusagent.config import Settings
4
+ from nexusagent.runtime import Runtime
5
+ from nexusagent.runtime_factory import create_tool_runtime
6
+
7
+ __all__ = ["Runtime", "create_application_runtime"]
8
+
9
+
10
+ def create_application_runtime(settings: Settings | None = None) -> Runtime:
11
+ return create_tool_runtime(settings)
@@ -0,0 +1,42 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+
5
+ from nexusagent.tool import Tool
6
+
7
+ _EXPRESSION_PATTERN = re.compile(r"\s*(-?\d+(?:\.\d+)?)\s*([+\-*/])\s*(-?\d+(?:\.\d+)?)\s*")
8
+
9
+
10
+ class CalculatorTool(Tool):
11
+ def __init__(self) -> None:
12
+ super().__init__(name="calculator", description="Performs basic arithmetic operations.")
13
+
14
+ def execute(self, input_data: str) -> str:
15
+ match = _EXPRESSION_PATTERN.fullmatch(input_data)
16
+ if not match:
17
+ raise ValueError(
18
+ "Invalid expression: expected 'number operator number' with operator one of + - * /"
19
+ )
20
+
21
+ left = float(match.group(1))
22
+ operator = match.group(2)
23
+ right = float(match.group(3))
24
+
25
+ if operator == "+":
26
+ result = left + right
27
+ elif operator == "-":
28
+ result = left - right
29
+ elif operator == "*":
30
+ result = left * right
31
+ else:
32
+ if right == 0:
33
+ raise ZeroDivisionError("Division by zero is not allowed")
34
+ result = left / right
35
+
36
+ return _format_result(result)
37
+
38
+
39
+ def _format_result(value: float) -> str:
40
+ if value.is_integer():
41
+ return str(int(value))
42
+ return str(value)
nexusagent/config.py ADDED
@@ -0,0 +1,81 @@
1
+ """Application configuration for NexusAgent.
2
+
3
+ Settings are read from environment variables. No .env loading and no
4
+ external configuration frameworks are used at this stage.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import math
10
+ import os
11
+ from dataclasses import dataclass
12
+ from urllib.parse import urlparse
13
+
14
+ SUPPORTED_PROVIDERS = {"fake", "http"}
15
+
16
+
17
+ @dataclass(frozen=True, repr=False)
18
+ class Settings:
19
+ """Basic application settings, loaded from environment variables."""
20
+
21
+ env: str = "development"
22
+ log_level: str = "INFO"
23
+ model: str | None = None
24
+ api_key: str | None = None
25
+ provider: str = "fake"
26
+ endpoint: str | None = None
27
+ timeout: float = 30.0
28
+
29
+ @classmethod
30
+ def from_env(cls) -> Settings:
31
+ """Build Settings from the current process environment."""
32
+ raw_timeout = os.getenv("NEXUS_TIMEOUT", "30.0")
33
+ try:
34
+ timeout = float(raw_timeout)
35
+ except ValueError:
36
+ raise ValueError(
37
+ f"Invalid NEXUS_TIMEOUT: {raw_timeout!r} — "
38
+ "must be a number of seconds (e.g. 30 or 4.5)"
39
+ ) from None
40
+
41
+ return cls(
42
+ env=os.getenv("NEXUS_ENV", "development"),
43
+ log_level=os.getenv("NEXUS_LOG_LEVEL", "INFO"),
44
+ model=os.getenv("NEXUS_MODEL"),
45
+ api_key=os.getenv("NEXUS_API_KEY"),
46
+ provider=os.getenv("NEXUS_PROVIDER", "fake"),
47
+ endpoint=os.getenv("NEXUS_ENDPOINT"),
48
+ timeout=timeout,
49
+ )
50
+
51
+ def __repr__(self) -> str:
52
+ api_key_display = "set" if self.api_key else "None"
53
+ return (
54
+ f"Settings(env={self.env!r}, log_level={self.log_level!r}, "
55
+ f"model={self.model!r}, api_key={api_key_display}, "
56
+ f"provider={self.provider!r}, endpoint={self.endpoint!r}, "
57
+ f"timeout={self.timeout!r})"
58
+ )
59
+
60
+ def validate(self) -> None:
61
+ """Validate the configuration locally, without any network access.
62
+
63
+ Raises ValueError with a clear, safe message on the first
64
+ problem found. Never includes the API key.
65
+ """
66
+ if self.provider not in SUPPORTED_PROVIDERS:
67
+ raise ValueError(f"Unsupported provider: {self.provider!r}")
68
+
69
+ if self.model is not None and not self.model.strip():
70
+ raise ValueError("Model must not be empty")
71
+
72
+ if not math.isfinite(self.timeout) or self.timeout <= 0:
73
+ raise ValueError("Timeout must be a positive finite number")
74
+
75
+ if self.provider == "http":
76
+ if not self.endpoint or not self.endpoint.strip():
77
+ raise ValueError("HTTP provider requires an endpoint")
78
+
79
+ scheme = urlparse(self.endpoint).scheme
80
+ if scheme not in ("http", "https"):
81
+ raise ValueError("Endpoint must use http or https")
@@ -0,0 +1,11 @@
1
+ from __future__ import annotations
2
+
3
+ from nexusagent.tool import Tool
4
+
5
+
6
+ class EchoTool(Tool):
7
+ def __init__(self) -> None:
8
+ super().__init__(name="echo", description="Returns its input unchanged.")
9
+
10
+ def execute(self, input_data: str) -> str:
11
+ return input_data
nexusagent/factory.py ADDED
@@ -0,0 +1,13 @@
1
+ from nexusagent.http_provider import HttpProvider
2
+ from nexusagent.provider import Provider, ProviderConfig, ProviderConfigurationError
3
+ from nexusagent.providers import FakeProvider
4
+
5
+
6
+ def create_provider(name: str, config: ProviderConfig | None = None) -> Provider:
7
+ if name == "fake":
8
+ return FakeProvider(config)
9
+
10
+ if name == "http":
11
+ return HttpProvider(config)
12
+
13
+ raise ProviderConfigurationError(f"Unsupported provider: {name!r}")
@@ -0,0 +1,189 @@
1
+ """Generic HTTP Provider.
2
+
3
+ Wire format (see docs/protocol.md):
4
+
5
+ Request ``POST <endpoint>`` with a JSON body::
6
+
7
+ {"model": ..., "input": "<prompt>"}
8
+
9
+ and, only when relevant, two extra keys::
10
+
11
+ "tools": [{"name": ..., "description": ...}, ...]
12
+ "tool_results": [{"tool_name": ..., "tool_input": ..., "output": ...,
13
+ "is_error": false}, ...]
14
+
15
+ Response a JSON object that is exactly one of::
16
+
17
+ {"output": "<text answer>"}
18
+ {"tool_name": "<tool>", "tool_input": "<input for the tool>"}
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import json
24
+ import urllib.error
25
+ import urllib.request
26
+ from collections.abc import Sequence
27
+
28
+ from nexusagent.provider import (
29
+ Provider,
30
+ ProviderConfig,
31
+ ProviderConfigurationError,
32
+ ProviderRequestError,
33
+ ProviderResponse,
34
+ ProviderResponseError,
35
+ TextResponse,
36
+ ToolCallRequest,
37
+ ToolResult,
38
+ ToolSpec,
39
+ )
40
+
41
+ _BODY_SNIPPET_MAX_CHARS = 200
42
+
43
+
44
+ def _response_body_snippet(exc: urllib.error.HTTPError) -> str:
45
+ """Return a short, single-line snippet of an HTTP error body (best effort)."""
46
+ try:
47
+ body = exc.read().decode("utf-8", errors="replace").strip()
48
+ except Exception: # noqa: BLE001 — the body is optional context, never fatal
49
+ return ""
50
+
51
+ if not body:
52
+ return ""
53
+
54
+ snippet = " ".join(body.split())
55
+ if len(snippet) > _BODY_SNIPPET_MAX_CHARS:
56
+ snippet = snippet[:_BODY_SNIPPET_MAX_CHARS] + "..."
57
+ return f": {snippet}"
58
+
59
+
60
+ class HttpProvider(Provider):
61
+ def __init__(self, config: ProviderConfig | None = None) -> None:
62
+ self.config = config or ProviderConfig()
63
+
64
+ def generate(self, prompt: str) -> str:
65
+ response = self.generate_response(prompt)
66
+
67
+ if isinstance(response, ToolCallRequest):
68
+ raise ProviderResponseError(
69
+ "HTTP response requested a tool call; use generate_response() to handle it"
70
+ )
71
+
72
+ return response.text
73
+
74
+ def generate_response(
75
+ self,
76
+ prompt: str,
77
+ *,
78
+ tools: Sequence[ToolSpec] = (),
79
+ tool_results: Sequence[ToolResult] = (),
80
+ ) -> ProviderResponse:
81
+ payload = self._post(self._build_body(prompt, tools, tool_results))
82
+ return self._parse_payload(payload)
83
+
84
+ def _build_body(
85
+ self,
86
+ prompt: str,
87
+ tools: Sequence[ToolSpec],
88
+ tool_results: Sequence[ToolResult],
89
+ ) -> dict[str, object]:
90
+ body: dict[str, object] = {"model": self.config.model, "input": prompt}
91
+
92
+ if tools:
93
+ body["tools"] = [{"name": t.name, "description": t.description} for t in tools]
94
+
95
+ if tool_results:
96
+ body["tool_results"] = [
97
+ {
98
+ "tool_name": r.tool_name,
99
+ "tool_input": r.tool_input,
100
+ "output": r.output,
101
+ "is_error": r.is_error,
102
+ }
103
+ for r in tool_results
104
+ ]
105
+
106
+ return body
107
+
108
+ def _post(self, body: dict[str, object]) -> object:
109
+ if not self.config.endpoint:
110
+ raise ProviderConfigurationError("HttpProvider requires a configured endpoint")
111
+
112
+ if self.config.timeout <= 0:
113
+ raise ProviderConfigurationError("HttpProvider requires a positive timeout")
114
+
115
+ headers = {"Content-Type": "application/json"}
116
+ if self.config.api_key:
117
+ headers["Authorization"] = f"Bearer {self.config.api_key}"
118
+
119
+ request = urllib.request.Request(
120
+ self.config.endpoint,
121
+ data=json.dumps(body).encode("utf-8"),
122
+ headers=headers,
123
+ method="POST",
124
+ )
125
+
126
+ try:
127
+ with urllib.request.urlopen(request, timeout=self.config.timeout) as response:
128
+ raw = response.read()
129
+ except urllib.error.HTTPError as exc:
130
+ # Include a short snippet of the response body when available: real
131
+ # backends put useful context there (rate limits, auth failures, ...).
132
+ detail = _response_body_snippet(exc)
133
+ raise ProviderRequestError(
134
+ f"HTTP request failed with status {exc.code}{detail}"
135
+ ) from None
136
+ except urllib.error.URLError as exc:
137
+ raise ProviderRequestError(f"HTTP request failed: {exc.reason}") from None
138
+ except TimeoutError:
139
+ raise ProviderRequestError("HTTP request timed out") from None
140
+
141
+ try:
142
+ return json.loads(raw)
143
+ except json.JSONDecodeError:
144
+ raise ProviderResponseError("HTTP response was not valid JSON") from None
145
+
146
+ @staticmethod
147
+ def _parse_payload(payload: object) -> ProviderResponse:
148
+ if not isinstance(payload, dict):
149
+ raise ProviderResponseError("HTTP response must be a JSON object")
150
+
151
+ has_output = "output" in payload
152
+ has_tool_call = "tool_name" in payload
153
+
154
+ if has_output and has_tool_call:
155
+ raise ProviderResponseError(
156
+ "HTTP response must contain either 'output' or 'tool_name', not both"
157
+ )
158
+
159
+ if has_tool_call:
160
+ return HttpProvider._parse_tool_call(payload)
161
+
162
+ if not has_output:
163
+ raise ProviderResponseError("HTTP response is missing the 'output' field")
164
+
165
+ output = payload["output"]
166
+ if not isinstance(output, str):
167
+ raise ProviderResponseError("HTTP response 'output' field must be a string")
168
+
169
+ if not output.strip():
170
+ raise ProviderResponseError("HTTP response 'output' field must not be empty")
171
+
172
+ return TextResponse(output)
173
+
174
+ @staticmethod
175
+ def _parse_tool_call(payload: dict[str, object]) -> ToolCallRequest:
176
+ tool_name = payload["tool_name"]
177
+ if not isinstance(tool_name, str) or not tool_name.strip():
178
+ raise ProviderResponseError(
179
+ "HTTP response 'tool_name' field must be a non-empty string"
180
+ )
181
+
182
+ if "tool_input" not in payload:
183
+ raise ProviderResponseError("HTTP response is missing the 'tool_input' field")
184
+
185
+ tool_input = payload["tool_input"]
186
+ if not isinstance(tool_input, str):
187
+ raise ProviderResponseError("HTTP response 'tool_input' field must be a string")
188
+
189
+ return ToolCallRequest(tool_name=tool_name, tool_input=tool_input)
nexusagent/logging.py ADDED
@@ -0,0 +1,60 @@
1
+ """Centralized logging setup for NexusAgent.
2
+
3
+ Provides get_logger() for obtaining namespaced loggers, and
4
+ configure_logging() to attach a single console handler with a
5
+ level derived from the existing Settings configuration.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import logging
11
+
12
+ from nexusagent.config import Settings
13
+
14
+ _ROOT_LOGGER_NAME = "nexusagent"
15
+ _LOG_FORMAT = "%(asctime)s | %(levelname)s | %(name)s | %(message)s"
16
+ _DATE_FORMAT = "%Y-%m-%d %H:%M:%S"
17
+
18
+
19
+ def get_logger(name: str | None = None) -> logging.Logger:
20
+ """Return a logger under the nexusagent namespace.
21
+
22
+ Without a name, returns the main "nexusagent" logger.
23
+ With a name (e.g. "nexusagent.agent"), returns that named logger.
24
+ """
25
+ if name is None:
26
+ return logging.getLogger(_ROOT_LOGGER_NAME)
27
+ return logging.getLogger(name)
28
+
29
+
30
+ def _resolve_level(level: str) -> int:
31
+ resolved = logging.getLevelName(level.upper())
32
+ if not isinstance(resolved, int):
33
+ raise ValueError(f"Invalid log level: {level!r}")
34
+ return resolved
35
+
36
+
37
+ def configure_logging(level: str | None = None) -> logging.Logger:
38
+ """Configure the nexusagent root logger with a console handler.
39
+
40
+ The log level defaults to the value provided by Settings
41
+ (NEXUS_LOG_LEVEL) unless explicitly overridden. Calling this
42
+ function multiple times is safe and will not attach duplicate
43
+ handlers.
44
+ """
45
+ if level is None:
46
+ level = Settings.from_env().log_level
47
+
48
+ resolved_level = _resolve_level(level)
49
+
50
+ logger = logging.getLogger(_ROOT_LOGGER_NAME)
51
+ logger.setLevel(resolved_level)
52
+ logger.propagate = False
53
+
54
+ has_console_handler = any(isinstance(h, logging.StreamHandler) for h in logger.handlers)
55
+ if not has_console_handler:
56
+ handler = logging.StreamHandler()
57
+ handler.setFormatter(logging.Formatter(_LOG_FORMAT, datefmt=_DATE_FORMAT))
58
+ logger.addHandler(handler)
59
+
60
+ return logger
nexusagent/main.py ADDED
@@ -0,0 +1,75 @@
1
+ import argparse
2
+ import sys
3
+ from dataclasses import replace
4
+
5
+ from nexusagent.application import Runtime, create_application_runtime
6
+ from nexusagent.config import Settings
7
+ from nexusagent.provider import ProviderError
8
+ from nexusagent.tool_executor import ToolExecutionError
9
+
10
+
11
+ def _build_tool_runtime() -> Runtime:
12
+ return create_application_runtime()
13
+
14
+
15
+ def _run_tool_list_command() -> None:
16
+ runtime = _build_tool_runtime()
17
+ for tool in runtime.list_tools():
18
+ print(f"{tool.name} - {tool.description}")
19
+
20
+
21
+ def _run_tool_command(argv: list[str]) -> None:
22
+ parser = argparse.ArgumentParser(prog="nexusagent tool")
23
+ subparsers = parser.add_subparsers(dest="tool_command", required=True)
24
+
25
+ run_parser = subparsers.add_parser("run", help="run a registered tool")
26
+ run_parser.add_argument("tool_name", help="name of the tool to run")
27
+ run_parser.add_argument("input_data", help="input passed to the tool")
28
+
29
+ subparsers.add_parser("list", help="list registered tools")
30
+
31
+ args = parser.parse_args(argv)
32
+
33
+ if args.tool_command == "list":
34
+ _run_tool_list_command()
35
+ return
36
+
37
+ runtime = _build_tool_runtime()
38
+
39
+ try:
40
+ result = runtime.execute_tool(args.tool_name, args.input_data)
41
+ except (KeyError, TypeError, ValueError, ToolExecutionError, RuntimeError) as exc:
42
+ print(f"nexusagent: error: {exc}", file=sys.stderr)
43
+ sys.exit(1)
44
+
45
+ print(result)
46
+
47
+
48
+ def main() -> None:
49
+ argv = sys.argv[1:]
50
+
51
+ if argv and argv[0] == "tool":
52
+ _run_tool_command(argv[1:])
53
+ return
54
+
55
+ parser = argparse.ArgumentParser(prog="nexusagent")
56
+ parser.add_argument("input_text", help="input text to run through the agent")
57
+ parser.add_argument("--provider", help="provider to use (overrides NEXUS_PROVIDER)")
58
+ args = parser.parse_args(argv)
59
+
60
+ settings = Settings.from_env()
61
+ if args.provider is not None:
62
+ settings = replace(settings, provider=args.provider)
63
+
64
+ try:
65
+ runtime = create_application_runtime(settings)
66
+ except (ValueError, ProviderError) as exc:
67
+ print(f"nexusagent: error: {exc}", file=sys.stderr)
68
+ sys.exit(1)
69
+
70
+ result = runtime.run(args.input_text)
71
+ if not result.success:
72
+ print(f"nexusagent: error: {result.output}", file=sys.stderr)
73
+ sys.exit(1)
74
+
75
+ print(result.output)
nexusagent/provider.py ADDED
@@ -0,0 +1,101 @@
1
+ from __future__ import annotations
2
+
3
+ from abc import ABC, abstractmethod
4
+ from collections.abc import Sequence
5
+ from dataclasses import dataclass
6
+
7
+
8
+ @dataclass(frozen=True, repr=False)
9
+ class ProviderConfig:
10
+ model: str | None = None
11
+ api_key: str | None = None
12
+ endpoint: str | None = None
13
+ timeout: float = 30.0
14
+
15
+ def __repr__(self) -> str:
16
+ api_key_display = "set" if self.api_key else "None"
17
+ return (
18
+ f"ProviderConfig(model={self.model!r}, api_key={api_key_display}, "
19
+ f"endpoint={self.endpoint!r}, timeout={self.timeout!r})"
20
+ )
21
+
22
+
23
+ @dataclass(frozen=True)
24
+ class TextResponse:
25
+ """A plain text response from a Provider."""
26
+
27
+ text: str
28
+
29
+
30
+ @dataclass(frozen=True)
31
+ class ToolCallRequest:
32
+ """A request from a Provider to call a specific Tool."""
33
+
34
+ tool_name: str
35
+ tool_input: str
36
+
37
+
38
+ ProviderResponse = TextResponse | ToolCallRequest
39
+ """What a Provider can answer with: either a plain text answer or a request
40
+ to call a tool. This is what ``Provider.generate_response()`` returns and what
41
+ the Agent's tool loop consumes."""
42
+
43
+
44
+ @dataclass(frozen=True)
45
+ class ToolSpec:
46
+ """A tool advertised to the Provider so it knows what it may request."""
47
+
48
+ name: str
49
+ description: str
50
+
51
+
52
+ @dataclass(frozen=True)
53
+ class ToolResult:
54
+ """The outcome of a ``ToolCallRequest``, fed back to the Provider.
55
+
56
+ ``is_error`` is True when the tool could not be run or failed; ``output``
57
+ then holds the error message so the model can recover or explain.
58
+ """
59
+
60
+ tool_name: str
61
+ tool_input: str
62
+ output: str
63
+ is_error: bool = False
64
+
65
+
66
+ class Provider(ABC):
67
+ @abstractmethod
68
+ def generate(self, prompt: str) -> str: ...
69
+
70
+ def generate_response(
71
+ self,
72
+ prompt: str,
73
+ *,
74
+ tools: Sequence[ToolSpec] = (),
75
+ tool_results: Sequence[ToolResult] = (),
76
+ ) -> ProviderResponse:
77
+ """Return a structured response for ``prompt``.
78
+
79
+ ``tools`` lists the tools the Provider may request; ``tool_results``
80
+ holds the results of tool calls it already requested in this run, in
81
+ order. The default implementation wraps ``generate()`` and therefore
82
+ only ever answers with text, so text-only Providers need no changes.
83
+ Providers that can request tools override this method.
84
+ """
85
+ return TextResponse(self.generate(prompt))
86
+
87
+
88
+ class ProviderError(Exception):
89
+ """Base exception for provider-related failures."""
90
+
91
+
92
+ class ProviderConfigurationError(ProviderError):
93
+ """Raised when provider configuration is invalid for a provider."""
94
+
95
+
96
+ class ProviderRequestError(ProviderError):
97
+ """Raised when a provider request fails."""
98
+
99
+
100
+ class ProviderResponseError(ProviderError):
101
+ """Raised when a provider response is invalid."""
@@ -0,0 +1,58 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Iterable, Sequence
4
+
5
+ from nexusagent.provider import (
6
+ Provider,
7
+ ProviderConfig,
8
+ ProviderResponse,
9
+ ProviderResponseError,
10
+ TextResponse,
11
+ ToolResult,
12
+ ToolSpec,
13
+ )
14
+
15
+
16
+ class FakeProvider(Provider):
17
+ def __init__(self, config: ProviderConfig | None = None) -> None:
18
+ self.config = config or ProviderConfig()
19
+
20
+ def generate(self, prompt: str) -> str:
21
+ return f"fake response: {prompt}"
22
+
23
+
24
+ class ScriptedProvider(Provider):
25
+ """A deterministic, network-free Provider that replays a fixed script.
26
+
27
+ Each call to ``generate_response()`` returns the next scripted
28
+ ``ProviderResponse``. It is meant for demos and tests of the tool loop:
29
+ script a ``ToolCallRequest`` followed by a ``TextResponse`` and the Agent
30
+ will run the tool and finish. Every call is recorded in ``calls``.
31
+ """
32
+
33
+ def __init__(self, responses: Iterable[ProviderResponse]) -> None:
34
+ self._responses = list(responses)
35
+ self._position = 0
36
+ self.calls: list[tuple[str, tuple[ToolSpec, ...], tuple[ToolResult, ...]]] = []
37
+
38
+ def generate(self, prompt: str) -> str:
39
+ response = self.generate_response(prompt)
40
+ if not isinstance(response, TextResponse):
41
+ raise ProviderResponseError("scripted response is a tool call, not text")
42
+ return response.text
43
+
44
+ def generate_response(
45
+ self,
46
+ prompt: str,
47
+ *,
48
+ tools: Sequence[ToolSpec] = (),
49
+ tool_results: Sequence[ToolResult] = (),
50
+ ) -> ProviderResponse:
51
+ self.calls.append((prompt, tuple(tools), tuple(tool_results)))
52
+
53
+ if self._position >= len(self._responses):
54
+ raise ProviderResponseError("ScriptedProvider has no more scripted responses")
55
+
56
+ response = self._responses[self._position]
57
+ self._position += 1
58
+ return response
nexusagent/py.typed ADDED
File without changes
nexusagent/runtime.py ADDED
@@ -0,0 +1,54 @@
1
+ from __future__ import annotations
2
+
3
+ from nexusagent.agent import Agent, AgentResult
4
+ from nexusagent.config import Settings
5
+ from nexusagent.factory import create_provider
6
+ from nexusagent.provider import ProviderConfig
7
+ from nexusagent.tool import Tool
8
+ from nexusagent.tool_executor import ToolExecutor
9
+ from nexusagent.tool_registry import ToolRegistry
10
+
11
+
12
+ class Runtime:
13
+ def __init__(
14
+ self,
15
+ agent: Agent,
16
+ tool_registry: ToolRegistry | None = None,
17
+ tool_executor: ToolExecutor | None = None,
18
+ ) -> None:
19
+ self.agent = agent
20
+ self.tool_registry = tool_registry
21
+ self.tool_executor = tool_executor
22
+
23
+ def run(self, input_text: str) -> AgentResult:
24
+ return self.agent.run(input_text)
25
+
26
+ def execute_tool(self, tool_name: str, input_data: str) -> str:
27
+ return self.agent.execute_tool(tool_name, input_data)
28
+
29
+ def list_tools(self) -> list[Tool]:
30
+ if self.tool_registry is None:
31
+ raise RuntimeError("Runtime requires a ToolRegistry to list tools")
32
+
33
+ return self.tool_registry.list_tools()
34
+
35
+
36
+ def create_runtime(
37
+ settings: Settings | None = None,
38
+ tool_registry: ToolRegistry | None = None,
39
+ tool_executor: ToolExecutor | None = None,
40
+ ) -> Runtime:
41
+ if settings is None:
42
+ settings = Settings.from_env()
43
+
44
+ settings.validate()
45
+
46
+ config = ProviderConfig(
47
+ model=settings.model,
48
+ api_key=settings.api_key,
49
+ endpoint=settings.endpoint,
50
+ timeout=settings.timeout,
51
+ )
52
+ provider = create_provider(settings.provider, config)
53
+ agent = Agent(provider, tool_registry=tool_registry, tool_executor=tool_executor)
54
+ return Runtime(agent, tool_registry=tool_registry, tool_executor=tool_executor)
@@ -0,0 +1,12 @@
1
+ from __future__ import annotations
2
+
3
+ from nexusagent.config import Settings
4
+ from nexusagent.runtime import Runtime, create_runtime
5
+ from nexusagent.tool_executor import ToolExecutor
6
+ from nexusagent.tool_factory import create_tool_registry
7
+
8
+
9
+ def create_tool_runtime(settings: Settings | None = None) -> Runtime:
10
+ registry = create_tool_registry()
11
+ executor = ToolExecutor()
12
+ return create_runtime(settings, tool_registry=registry, tool_executor=executor)
nexusagent/tool.py ADDED
@@ -0,0 +1,24 @@
1
+ from __future__ import annotations
2
+
3
+ from abc import ABC, abstractmethod
4
+
5
+
6
+ class Tool(ABC):
7
+ def __init__(self, name: str, description: str) -> None:
8
+ if not isinstance(name, str):
9
+ raise TypeError("Tool name must be a string")
10
+
11
+ if not name.strip():
12
+ raise ValueError("Tool name must not be empty or whitespace-only")
13
+
14
+ if not isinstance(description, str):
15
+ raise TypeError("Tool description must be a string")
16
+
17
+ if not description.strip():
18
+ raise ValueError("Tool description must not be empty or whitespace-only")
19
+
20
+ self.name = name
21
+ self.description = description
22
+
23
+ @abstractmethod
24
+ def execute(self, input_data: str) -> str: ...
@@ -0,0 +1,26 @@
1
+ from __future__ import annotations
2
+
3
+ from nexusagent.tool import Tool
4
+
5
+
6
+ class ToolExecutionError(Exception):
7
+ """Raised when a Tool's execute() call fails."""
8
+
9
+
10
+ class ToolExecutor:
11
+ def execute(self, tool: Tool, input_data: str) -> str:
12
+ if not isinstance(tool, Tool):
13
+ raise TypeError("ToolExecutor requires a Tool instance")
14
+
15
+ if not isinstance(input_data, str):
16
+ raise TypeError("Tool input must be a string")
17
+
18
+ if not input_data.strip():
19
+ raise ValueError("Tool input must not be empty or whitespace-only")
20
+
21
+ try:
22
+ return tool.execute(input_data)
23
+ except Exception as exc:
24
+ # Preserve the original error class in the message: the chain keeps the
25
+ # exception itself, but callers often only see the ToolExecutionError text.
26
+ raise ToolExecutionError(f"{type(exc).__name__}: {exc}") from exc
@@ -0,0 +1,14 @@
1
+ from __future__ import annotations
2
+
3
+ from nexusagent.calculator_tool import CalculatorTool
4
+ from nexusagent.echo_tool import EchoTool
5
+ from nexusagent.tool_registry import ToolRegistry
6
+ from nexusagent.uppercase_tool import UppercaseTool
7
+
8
+
9
+ def create_tool_registry() -> ToolRegistry:
10
+ registry = ToolRegistry()
11
+ registry.register(EchoTool())
12
+ registry.register(UppercaseTool())
13
+ registry.register(CalculatorTool())
14
+ return registry
@@ -0,0 +1,49 @@
1
+ from __future__ import annotations
2
+
3
+ from nexusagent.tool import Tool
4
+
5
+
6
+ class ToolRegistry:
7
+ def __init__(self) -> None:
8
+ self._tools: dict[str, Tool] = {}
9
+
10
+ def register(self, tool: Tool) -> None:
11
+ if not isinstance(tool, Tool):
12
+ raise TypeError("ToolRegistry requires a Tool instance")
13
+
14
+ if tool.name in self._tools:
15
+ raise ValueError(f"Tool {tool.name!r} is already registered")
16
+
17
+ self._tools[tool.name] = tool
18
+
19
+ def get(self, name: str) -> Tool:
20
+ self._validate_name(name)
21
+
22
+ if name not in self._tools:
23
+ raise KeyError(f"Tool {name!r} is not registered")
24
+
25
+ return self._tools[name]
26
+
27
+ def has(self, name: str) -> bool:
28
+ self._validate_name(name)
29
+
30
+ return name in self._tools
31
+
32
+ def unregister(self, name: str) -> None:
33
+ self._validate_name(name)
34
+
35
+ if name not in self._tools:
36
+ raise KeyError(f"Tool {name!r} is not registered")
37
+
38
+ del self._tools[name]
39
+
40
+ def list_tools(self) -> list[Tool]:
41
+ return list(self._tools.values())
42
+
43
+ @staticmethod
44
+ def _validate_name(name: str) -> None:
45
+ if not isinstance(name, str):
46
+ raise TypeError("Tool name must be a string")
47
+
48
+ if not name.strip():
49
+ raise ValueError("Tool name must not be empty or whitespace-only")
@@ -0,0 +1,11 @@
1
+ from __future__ import annotations
2
+
3
+ from nexusagent.tool import Tool
4
+
5
+
6
+ class UppercaseTool(Tool):
7
+ def __init__(self) -> None:
8
+ super().__init__(name="uppercase", description="Converts input to uppercase.")
9
+
10
+ def execute(self, input_data: str) -> str:
11
+ return input_data.upper()
@@ -0,0 +1,192 @@
1
+ Metadata-Version: 2.4
2
+ Name: nexusagent-runtime
3
+ Version: 0.4.0
4
+ Summary: A small, modular AI agent runtime in Python — agents, providers, tools and a readable core.
5
+ License-Expression: MIT
6
+ Project-URL: Homepage, https://github.com/Abolfazlrwm/nexusagent
7
+ Project-URL: Repository, https://github.com/Abolfazlrwm/nexusagent
8
+ Project-URL: Changelog, https://github.com/Abolfazlrwm/nexusagent/blob/main/CHANGELOG.md
9
+ Project-URL: Issues, https://github.com/Abolfazlrwm/nexusagent/issues
10
+ Keywords: ai-agents,agent-runtime,llm,tools,providers
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
20
+ Classifier: Typing :: Typed
21
+ Requires-Python: >=3.10
22
+ Description-Content-Type: text/markdown
23
+ License-File: LICENSE
24
+ Provides-Extra: dev
25
+ Requires-Dist: pytest>=8.0; extra == "dev"
26
+ Requires-Dist: pytest-cov>=5.0; extra == "dev"
27
+ Requires-Dist: mypy>=1.10; extra == "dev"
28
+ Requires-Dist: ruff>=0.6; extra == "dev"
29
+ Requires-Dist: pre-commit>=3.7; extra == "dev"
30
+ Requires-Dist: build>=1.2; extra == "dev"
31
+ Dynamic: license-file
32
+
33
+ # NexusAgent
34
+
35
+ [![CI](https://github.com/Abolfazlrwm/nexusagent/actions/workflows/ci.yml/badge.svg)](https://github.com/Abolfazlrwm/nexusagent/actions/workflows/ci.yml)
36
+ [![Coverage](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/Abolfazlrwm/nexusagent/gh-pages/coverage-badge.json)](https://github.com/Abolfazlrwm/nexusagent/actions/workflows/ci.yml)
37
+ [![PyPI](https://img.shields.io/pypi/v/nexusagent-runtime)](https://pypi.org/project/nexusagent-runtime/)
38
+ [![Python](https://img.shields.io/pypi/pyversions/nexusagent-runtime)](https://pypi.org/project/nexusagent-runtime/)
39
+ [![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
40
+
41
+ **A modular runtime for AI agents in Python — zero runtime dependencies, a small readable core, and an honest feature list.**
42
+
43
+ Most agent frameworks hand you a galaxy of abstractions. NexusAgent is the opposite: ~500 lines of typed, contract-tested core that you can read in one sitting — providers, tools, a bounded autonomous tool loop, and a CLI. Nothing hidden, nothing magic.
44
+
45
+ ```python
46
+ from nexusagent import create_application_runtime
47
+
48
+ runtime = create_application_runtime() # offline `fake` provider by default
49
+
50
+ result = runtime.run("Hello NexusAgent")
51
+ print(result.output) # fake response: Hello NexusAgent
52
+ print(result.success) # True
53
+
54
+ print(runtime.execute_tool("calculator", "2 + 3")) # 5
55
+ ```
56
+
57
+ ## Why NexusAgent
58
+
59
+ - **Zero runtime dependencies** — the whole core is the Python standard library. `pip install nexusagent-runtime` pulls nothing else.
60
+ - **Readable by design** — the core is small enough to audit in an afternoon; architecture rules are enforced by tests, not comments.
61
+ - **Honest** — the feature list below says what exists *and what doesn't*. No fake benchmarks, no aspirational docs.
62
+ - **Typed** — ships `py.typed`; the source itself passes `mypy --strict`.
63
+ - **Tested** — 600+ contract tests, ≥95% coverage floor enforced in CI.
64
+
65
+ ## Features
66
+
67
+ | Capability | Status |
68
+ | --- | --- |
69
+ | Provider abstraction (bring any backend) | ✅ |
70
+ | Autonomous tool loop, bounded by `max_iterations` | ✅ |
71
+ | Vendor-neutral HTTP wire protocol | ✅ [docs/protocol.md](docs/protocol.md) |
72
+ | Built-in tools: `echo`, `uppercase`, `calculator` | ✅ |
73
+ | Custom tools / custom providers via small ABCs | ✅ [guides](docs/) |
74
+ | Configuration via `NEXUS_*` environment variables | ✅ |
75
+ | CLI (`nexusagent`, `python -m nexusagent`) | ✅ |
76
+ | Fully typed (`py.typed`), strict mypy clean | ✅ |
77
+ | Async execution / streaming | ❌ |
78
+ | Memory, planning, multi-agent orchestration | ❌ |
79
+ | Vendor adapters (OpenAI, Anthropic, …) | ❌ |
80
+ | Retries / backoff on HTTP failures | ❌ |
81
+
82
+ ## Architecture
83
+
84
+ ```mermaid
85
+ flowchart LR
86
+ U["your code / CLI"] --> R["Runtime"]
87
+ R --> A["Agent"]
88
+ A -- "generate_response()" --> P["Provider<br/>(fake · scripted · http)"]
89
+ P -- "TextResponse" --> A
90
+ P -- "ToolCallRequest" --> A
91
+ A --> X["ToolExecutor"]
92
+ X --> T["ToolRegistry<br/>echo · uppercase · calculator"]
93
+ X -- "ToolResult" --> A
94
+ ```
95
+
96
+ The Provider answers with text or requests a tool; the Agent runs the tool
97
+ and feeds the result back, repeating until a text answer or the iteration
98
+ limit. Deeper details: [docs/architecture.md](docs/architecture.md) ·
99
+ concepts: [docs/concepts.md](docs/concepts.md) · decisions:
100
+ [docs/adr/](docs/adr/).
101
+
102
+ ## Installation
103
+
104
+ ```bash
105
+ pip install nexusagent-runtime
106
+ ```
107
+
108
+ > The **import name** is `nexusagent`; the **PyPI distribution** is
109
+ > `nexusagent-runtime` — the exact name `nexusagent` is blocked on PyPI by an
110
+ > unrelated placeholder package (`nexus-agent`), see
111
+ > [ADR 0004](docs/adr/0004-pypi-distribution-name.md).
112
+
113
+ Requires Python 3.10–3.13. From source:
114
+
115
+ ```bash
116
+ git clone https://github.com/Abolfazlrwm/nexusagent
117
+ cd nexusagent
118
+ pip install -e ".[dev]"
119
+ python scripts/check.py # lint + format + mypy --strict + tests
120
+ ```
121
+
122
+ ## Usage
123
+
124
+ ### Agent with tools
125
+
126
+ ```python
127
+ from nexusagent import create_application_runtime
128
+
129
+ runtime = create_application_runtime()
130
+
131
+ print(runtime.list_tools())
132
+ # [EchoTool, UppercaseTool, CalculatorTool]
133
+
134
+ # The provider decides when to request a tool; the agent runs the loop.
135
+ result = runtime.run("What is 19 + 23?")
136
+ ```
137
+
138
+ ### The tool loop, end to end, offline
139
+
140
+ ```bash
141
+ python examples/tool_loop_demo.py
142
+ ```
143
+
144
+ Starts a local "model backend" speaking the [HTTP protocol](docs/protocol.md),
145
+ requests the calculator, and turns the result into a final answer — no API
146
+ key, no internet.
147
+
148
+ ### CLI
149
+
150
+ ```bash
151
+ nexusagent "your prompt" # or: python -m nexusagent "your prompt"
152
+ nexusagent tool list
153
+ nexusagent tool run calculator "2 + 3"
154
+ ```
155
+
156
+ ### Configuration
157
+
158
+ | Variable | Meaning | Default |
159
+ | --- | --- | --- |
160
+ | `NEXUS_PROVIDER` | `fake` or `http` | `fake` |
161
+ | `NEXUS_ENDPOINT` | HTTP provider URL | — |
162
+ | `NEXUS_MODEL` | model identifier passed to the provider | — |
163
+ | `NEXUS_API_KEY` | sent as `Bearer` token (never logged, masked in reprs) | — |
164
+ | `NEXUS_TIMEOUT` | HTTP timeout in seconds | `30` |
165
+
166
+ ## Examples
167
+
168
+ All examples run offline, with no API keys:
169
+
170
+ - [`examples/basic.py`](examples/basic.py) — smallest possible run
171
+ - [`examples/custom_tool.py`](examples/custom_tool.py) — define and register your own tool
172
+ - [`examples/tool_loop_demo.py`](examples/tool_loop_demo.py) — the autonomous tool loop over local HTTP
173
+ - [`examples/http_provider_local.py`](examples/http_provider_local.py) — `http` provider against a local backend
174
+
175
+ ## Documentation
176
+
177
+ - [Architecture](docs/architecture.md) — layers, module map, dependency rules
178
+ - [Concepts](docs/concepts.md) — Agent, Provider, Tool, Runtime, Settings
179
+ - [Guides](docs/) — write a custom [provider](docs/guides/custom-provider.md) or [tool](docs/guides/custom-tool.md)
180
+ - [HTTP protocol](docs/protocol.md) — wire format for backends
181
+ - [ADRs](docs/adr/) — architecture decision records
182
+ - [Security policy](SECURITY.md) · [Changelog](CHANGELOG.md) · [Contributing](CONTRIBUTING.md)
183
+
184
+ ## Project status
185
+
186
+ Actively developed, pre-1.0. See [PROJECT_STATUS.md](PROJECT_STATUS.md) for
187
+ a candid snapshot of what works and what the next steps are. Breaking public
188
+ API changes require an ADR and are discussed in the open.
189
+
190
+ ## License
191
+
192
+ [MIT](LICENSE)
@@ -0,0 +1,27 @@
1
+ nexusagent/__init__.py,sha256=7FZQXiKE_ZNHrX2qrDVHIccab94TZQ28_UTZ0qw5oWI,361
2
+ nexusagent/__main__.py,sha256=e85R9MeC3LyBNDBOd8UPQkCPUWBMxbAuM5AU-T90C4Y,41
3
+ nexusagent/agent.py,sha256=9k2fQAOW-nCp-b7d9RXHi-E4EpGTVDDv6qS33wRzGhs,6806
4
+ nexusagent/application.py,sha256=rDHWdTFLSCVcjH-aSgxfg4Zb8sTPtJ_yLsNwOiS2Ygg,346
5
+ nexusagent/calculator_tool.py,sha256=IlP5QmH29_5a3aA9ylK9ASRfbcX2f0qvR6YrZ6RdNo0,1219
6
+ nexusagent/config.py,sha256=8Mu8StfwXYI_-GuUdmy-r71sJWmWXWfdjuiwUZZWaYw,2819
7
+ nexusagent/echo_tool.py,sha256=Cu6B4oAdtZ9r1cyrsc_NSlEfYbxQfpp-ncwXcQ3tyj4,281
8
+ nexusagent/factory.py,sha256=F-Lv9V0Z431jwBWvIedkEHlAqyJAg8summ-jdaFrnCc,457
9
+ nexusagent/http_provider.py,sha256=5WPRdzEthckQjothOH1csHo_S3Ri1aKY6xSuFIMCHHY,6292
10
+ nexusagent/logging.py,sha256=2DiDJezI0luO79Krq7MlwIJ3PqauOtbGFbFNaZcR-HM,1912
11
+ nexusagent/main.py,sha256=yZfbMUneBSUCgtb2vAj_zXYt0RkQDuxr73vOXNEi6SI,2323
12
+ nexusagent/provider.py,sha256=ef1_s7g6YNbiZfrdqi79TE6rMJkMvdbC5roxr_yAb0c,2779
13
+ nexusagent/providers.py,sha256=39mX_gG3gQB0p9OslmqyeSl-8DG6dLteAYV-cqU0C4w,1899
14
+ nexusagent/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
15
+ nexusagent/runtime.py,sha256=Fq_uPrZBkwZXB94UB4C6xfuH7zrYtZlJPq9ZU9pJtjs,1739
16
+ nexusagent/runtime_factory.py,sha256=BAiiMpOViCZQCjS8Gy8GwA4vRmYqfSK-sFxQePnNEgs,461
17
+ nexusagent/tool.py,sha256=6I9Nxi0v7xxtkIlx3S_hV_SwoRjc-6JC9qgM2W_sixs,728
18
+ nexusagent/tool_executor.py,sha256=5-RXIIms2NV7cAsa16XYBtepG8RSqTD34f-OhkZnRXY,911
19
+ nexusagent/tool_factory.py,sha256=oOK_YlNeomycXxuIE4XSC0XHGOD9Z336SiDRZGen7TU,443
20
+ nexusagent/tool_registry.py,sha256=1nW77MWUce8pxFYKN_Bu9yncqtSZYS8bp5SSZsyKtSw,1346
21
+ nexusagent/uppercase_tool.py,sha256=jvIHQ9ebdNVDE6gAaRp7t0YldyCkrHqqoBiy8J4mnZ4,299
22
+ nexusagent_runtime-0.4.0.dist-info/licenses/LICENSE,sha256=YnGz4nqnN3q5lB6AqSa_-tpK4btjj0dolg8D7ZtIIFI,1069
23
+ nexusagent_runtime-0.4.0.dist-info/METADATA,sha256=f64Cca2IwtdGvmZksF0ug46ANy5lga9woMYuyXbaPp4,7622
24
+ nexusagent_runtime-0.4.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
25
+ nexusagent_runtime-0.4.0.dist-info/entry_points.txt,sha256=YksSV12J-TqF8zt9QMo78_EjZMmJgnkJLG8cio6TrZU,52
26
+ nexusagent_runtime-0.4.0.dist-info/top_level.txt,sha256=sXZ4p_tmv61VOgQG2VcxWV-PUmzCgOcewTKJhQLSPOA,11
27
+ nexusagent_runtime-0.4.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.2.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ nexusagent = nexusagent.main:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Abolfazl RWM
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ nexusagent