velophex 0.2.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 (43) hide show
  1. velophex/__init__.py +38 -0
  2. velophex/_json.py +297 -0
  3. velophex/agents/__init__.py +130 -0
  4. velophex/agents/_errors.py +75 -0
  5. velophex/agents/_loop.py +436 -0
  6. velophex/agents/_tools.py +168 -0
  7. velophex/agents/_tracing.py +87 -0
  8. velophex/agents/testing.py +249 -0
  9. velophex/cli/__init__.py +5 -0
  10. velophex/cli/main.py +548 -0
  11. velophex/cli/publish.py +152 -0
  12. velophex/cli/sync.py +90 -0
  13. velophex/cli/templates.py +126 -0
  14. velophex/contracts/__init__.py +103 -0
  15. velophex/contracts/_model.py +26 -0
  16. velophex/contracts/agents.py +110 -0
  17. velophex/contracts/diagnostics.py +157 -0
  18. velophex/contracts/executor.py +1921 -0
  19. velophex/contracts/package.py +391 -0
  20. velophex/contracts/project.py +1008 -0
  21. velophex/contracts/runtimes.py +226 -0
  22. velophex/contracts/signin.py +53 -0
  23. velophex/contracts/validation.py +158 -0
  24. velophex/entrypoints.py +329 -0
  25. velophex/errors.py +44 -0
  26. velophex/packaging.py +465 -0
  27. velophex/py.typed +0 -0
  28. velophex/runtime/__init__.py +471 -0
  29. velophex/runtime/_session.py +192 -0
  30. velophex/runtime/client.py +957 -0
  31. velophex/runtime/models.py +326 -0
  32. velophex/signing.py +425 -0
  33. velophex/worker/__init__.py +27 -0
  34. velophex/worker/__main__.py +118 -0
  35. velophex/worker/framing.py +248 -0
  36. velophex/worker/loader.py +105 -0
  37. velophex/worker/outbox.py +147 -0
  38. velophex/worker/session.py +519 -0
  39. velophex-0.2.0.dist-info/METADATA +313 -0
  40. velophex-0.2.0.dist-info/RECORD +43 -0
  41. velophex-0.2.0.dist-info/WHEEL +4 -0
  42. velophex-0.2.0.dist-info/entry_points.txt +2 -0
  43. velophex-0.2.0.dist-info/licenses/LICENSE +21 -0
velophex/__init__.py ADDED
@@ -0,0 +1,38 @@
1
+ """VeloPhex Python SDK: author, run, pack and publish Python automations.
2
+
3
+ A Python automation is an ordinary package with a ``project.json`` (ADR-P002) and one or more
4
+ functions marked with :func:`entrypoint`. ``velophex pack`` turns it into the same signed
5
+ ``.nupkg`` a Studio workflow produces, and the Runner executes it with ``python -m
6
+ velophex.worker`` over the executor protocol (ADR-P005, ADR-P008).
7
+
8
+ >>> import velophex
9
+ >>>
10
+ >>> @velophex.entrypoint(id="main")
11
+ ... def run(pdf_path: str) -> dict[str, float]:
12
+ ... if not pdf_path.endswith(".pdf"):
13
+ ... raise velophex.BusinessError("Only PDF invoices are accepted.")
14
+ ... return {"total": 42.5}
15
+
16
+ Submodules:
17
+
18
+ * :mod:`velophex.runtime` - assets, secrets, queues, child jobs and cooperative stop
19
+ * :mod:`velophex.contracts` - the platform contracts as Python models
20
+ * :mod:`velophex.worker` - the executor protocol worker; the Runner's entry point
21
+ * :mod:`velophex.cli` - ``velophex init | run | pack | publish``
22
+ """
23
+
24
+ from velophex.entrypoints import EntryPointDefinition, EntryPointInputError, entrypoint
25
+ from velophex.errors import BusinessError, VelophexError
26
+ from velophex.runtime import should_stop
27
+
28
+ __all__ = [
29
+ "BusinessError",
30
+ "EntryPointDefinition",
31
+ "EntryPointInputError",
32
+ "VelophexError",
33
+ "__version__",
34
+ "entrypoint",
35
+ "should_stop",
36
+ ]
37
+
38
+ __version__ = "0.2.0"
velophex/_json.py ADDED
@@ -0,0 +1,297 @@
1
+ r"""Byte-exact JSON compatible with ``VeloPhex.Platform.Contracts.Json.PlatformJson``.
2
+
3
+ Every VeloPhex tool must produce the *same bytes* for the same contract document:
4
+ ``velophex/package-manifest.json`` carries the SHA-512 of ``content/project.json``, so a
5
+ package written by the Python CLI and one written by Studio only verify against each other
6
+ when both writers agree down to the last escape. Python's :mod:`json` does not agree with
7
+ ``System.Text.Json``, which is why this module exists rather than a call to ``json.dumps``.
8
+
9
+ The four differences that matter, all taken from .NET 10 rather than from the documentation:
10
+
11
+ * ``System.Text.Json`` uses ``JavaScriptEncoder.Default``, which escapes the HTML-sensitive
12
+ ASCII characters ``"`` ``&`` ``'`` ``+`` ``<`` ``>`` and the backtick, plus ``\``, the
13
+ controls and everything outside printable ASCII, as ``\uXXXX`` with **upper-case** hex
14
+ digits (non-BMP characters as their surrogate pair; a lone surrogate as ``�``).
15
+ Python escapes almost nothing by default.
16
+ * Indented output is two spaces with LF endings and no space before the ``:``.
17
+ * A ``JsonElement`` (an opaque subtree such as an entry point's JSON Schema) is re-emitted
18
+ with its **original number literals**: ``1.0`` stays ``1.0`` and ``1e2`` stays ``1e2``.
19
+ :class:`RawInt` and :class:`RawFloat` carry the literal through a load/dump round trip.
20
+ * ``DateTimeOffset``, ``Guid`` and friends are written by typed fast paths that never reach
21
+ the encoder, so their ``+`` is not escaped. :class:`VerbatimString` says so.
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ import json
27
+ from collections.abc import Iterator, Mapping, Sequence
28
+ from decimal import Decimal
29
+ from typing import Any, Final
30
+
31
+ __all__ = [
32
+ "MAXIMUM_DEPTH",
33
+ "JsonValue",
34
+ "RawFloat",
35
+ "RawInt",
36
+ "VerbatimString",
37
+ "dumps_document",
38
+ "dumps_wire",
39
+ "encode_wire",
40
+ "escape_string",
41
+ "loads",
42
+ "omit_none",
43
+ ]
44
+
45
+ #: Mirrors ``PlatformJson.MaximumDepth``; documents nested deeper are rejected on read.
46
+ MAXIMUM_DEPTH: Final = 64
47
+
48
+ JsonValue = Any
49
+ """Any value the platform JSON writer accepts: ``None``, bool, int, float, str, list or dict."""
50
+
51
+ _INDENT: Final = " "
52
+
53
+ # U+FEFF, spelled as a code point so no editor can lose it to its own BOM handling.
54
+ _BOM: Final = chr(0xFEFF)
55
+
56
+ # Short escapes System.Text.Json emits instead of \uXXXX.
57
+ _SHORT_ESCAPES: Final[dict[int, str]] = {
58
+ 0x08: "\\b",
59
+ 0x09: "\\t",
60
+ 0x0A: "\\n",
61
+ 0x0C: "\\f",
62
+ 0x0D: "\\r",
63
+ 0x5C: "\\\\",
64
+ }
65
+
66
+ # Printable ASCII that JavaScriptEncoder.Default still escapes, because the encoder is safe
67
+ # for HTML and script contexts by default.
68
+ _HTML_SENSITIVE: Final[frozenset[int]] = frozenset({0x22, 0x26, 0x27, 0x2B, 0x3C, 0x3E, 0x60})
69
+
70
+
71
+ class RawInt(int):
72
+ """An integer JSON literal that remembers the text it was parsed from.
73
+
74
+ ``System.Text.Json`` writes a ``JsonElement`` number verbatim, so a schema authored as
75
+ ``1e2`` must not come back as ``100``. Instances behave as the number they stand for;
76
+ only the writer looks at :attr:`literal`.
77
+ """
78
+
79
+ # No __slots__: CPython forbids a non-empty one on an int or float subclass.
80
+ literal: str
81
+
82
+ def __new__(cls, literal: str) -> RawInt:
83
+ value = super().__new__(cls, int(literal))
84
+ value.literal = literal
85
+ return value
86
+
87
+ def __repr__(self) -> str:
88
+ return f"RawInt({self.literal!r})"
89
+
90
+
91
+ class VerbatimString(str):
92
+ r"""A string .NET writes without running it through the escaping encoder.
93
+
94
+ ``Utf8JsonWriter`` has typed fast paths for ``DateTimeOffset``, ``DateTime``, ``Guid`` and
95
+ ``TimeSpan`` that format straight into the buffer. The encoder never sees them, so a
96
+ ``DateTimeOffset`` of ``1970-01-01T00:00:00+00:00`` keeps its ``+`` where the same text in
97
+ a plain ``string`` property would come out as ``+``. The executor frame vectors pin
98
+ that difference, so the writer needs a way to say "this one is already formatted".
99
+
100
+ Only ever wrap machine-generated text: the contents are emitted between quotes as they
101
+ are, with no escaping at all.
102
+ """
103
+
104
+
105
+ class RawFloat(float):
106
+ """A fractional JSON literal that remembers the text it was parsed from."""
107
+
108
+ literal: str
109
+
110
+ def __new__(cls, literal: str) -> RawFloat:
111
+ value = super().__new__(cls, float(literal))
112
+ value.literal = literal
113
+ return value
114
+
115
+ def __repr__(self) -> str:
116
+ return f"RawFloat({self.literal!r})"
117
+
118
+
119
+ def omit_none(members: dict[str, JsonValue]) -> dict[str, JsonValue]:
120
+ """Drop null members, matching .NET's ``JsonIgnoreCondition.WhenWritingNull``.
121
+
122
+ Every contract body is built this way rather than by filtering afterwards, so the member
123
+ order of the .NET record stays visible in the source.
124
+ """
125
+ return {name: value for name, value in members.items() if value is not None}
126
+
127
+
128
+ def escape_string(value: str) -> str:
129
+ """Escape *value* exactly as ``JavaScriptEncoder.Default`` does, without the quotes."""
130
+ pieces: list[str] = []
131
+
132
+ for character in value:
133
+ code = ord(character)
134
+
135
+ if code < 0x20 or code == 0x7F or code in _HTML_SENSITIVE or code == 0x5C:
136
+ short = _SHORT_ESCAPES.get(code)
137
+ pieces.append(short if short is not None else f"\\u{code:04X}")
138
+ elif code < 0x7F:
139
+ pieces.append(character)
140
+ elif code > 0xFFFF:
141
+ # Non-BMP scalars are escaped as the UTF-16 surrogate pair, as .NET does.
142
+ offset = code - 0x10000
143
+ pieces.append(f"\\u{0xD800 + (offset >> 10):04X}\\u{0xDC00 + (offset & 0x3FF):04X}")
144
+ elif 0xD800 <= code <= 0xDFFF:
145
+ # A lone surrogate (Python keeps them, e.g. from surrogateescape) is not valid
146
+ # UTF-16; .NET writes U+FFFD in its place rather than failing.
147
+ pieces.append("\\uFFFD")
148
+ else:
149
+ pieces.append(f"\\u{code:04X}")
150
+
151
+ return "".join(pieces)
152
+
153
+
154
+ def _format_number(value: object) -> str:
155
+ literal = getattr(value, "literal", None)
156
+
157
+ if isinstance(literal, str):
158
+ return literal
159
+
160
+ if isinstance(value, bool): # bool is an int; callers should never reach here
161
+ raise TypeError("Booleans are not numbers in JSON.")
162
+
163
+ if isinstance(value, int):
164
+ return str(value)
165
+
166
+ if isinstance(value, Decimal):
167
+ return format(value, "f")
168
+
169
+ number = float(value)
170
+
171
+ if number != number or number in (float("inf"), float("-inf")):
172
+ raise ValueError(f"{value!r} has no JSON representation.")
173
+
174
+ # repr() gives Python's shortest round-trip form, which matches .NET for every value the
175
+ # contracts carry (sizes, timeouts, schema bounds). Exponential literals differ in case
176
+ # and padding, so a document that needs one must keep its RawNumber.
177
+ return repr(number)
178
+
179
+
180
+ def _write(value: JsonValue, out: list[str], indent: int | None, depth: int) -> None:
181
+ if depth > MAXIMUM_DEPTH:
182
+ raise ValueError(f"The document is nested deeper than {MAXIMUM_DEPTH} levels.")
183
+
184
+ if value is None:
185
+ out.append("null")
186
+ elif value is True:
187
+ out.append("true")
188
+ elif value is False:
189
+ out.append("false")
190
+ elif isinstance(value, VerbatimString):
191
+ out.append(f'"{value}"')
192
+ elif isinstance(value, str):
193
+ out.append(f'"{escape_string(value)}"')
194
+ elif isinstance(value, (int, float, Decimal)):
195
+ out.append(_format_number(value))
196
+ elif isinstance(value, Mapping):
197
+ _write_object(value, out, indent, depth)
198
+ elif isinstance(value, Sequence):
199
+ _write_array(value, out, indent, depth)
200
+ else:
201
+ raise TypeError(f"{type(value).__name__} is not a JSON value.")
202
+
203
+
204
+ def _write_object(value: Mapping[str, JsonValue], out: list[str], indent: int | None, depth: int) -> None:
205
+ items = list(value.items())
206
+
207
+ if not items:
208
+ out.append("{}")
209
+ return
210
+
211
+ inner = None if indent is None else indent + 1
212
+ out.append("{")
213
+
214
+ for index, (name, member) in enumerate(items):
215
+ if index:
216
+ out.append(",")
217
+ if inner is not None:
218
+ out.append("\n" + _INDENT * inner)
219
+ out.append(f'"{escape_string(str(name))}":')
220
+ if inner is not None:
221
+ out.append(" ")
222
+ _write(member, out, inner, depth + 1)
223
+
224
+ if indent is not None:
225
+ out.append("\n" + _INDENT * indent)
226
+
227
+ out.append("}")
228
+
229
+
230
+ def _write_array(value: Sequence[JsonValue], out: list[str], indent: int | None, depth: int) -> None:
231
+ items = list(value)
232
+
233
+ if not items:
234
+ out.append("[]")
235
+ return
236
+
237
+ inner = None if indent is None else indent + 1
238
+ out.append("[")
239
+
240
+ for index, member in enumerate(items):
241
+ if index:
242
+ out.append(",")
243
+ if inner is not None:
244
+ out.append("\n" + _INDENT * inner)
245
+ _write(member, out, inner, depth + 1)
246
+
247
+ if indent is not None:
248
+ out.append("\n" + _INDENT * indent)
249
+
250
+ out.append("]")
251
+
252
+
253
+ def dumps_document(value: JsonValue) -> str:
254
+ """Serialize *value* the way the platform writes files: 2-space indent, LF, no trailing newline."""
255
+ out: list[str] = []
256
+ _write(value, out, 0, 0)
257
+ return "".join(out)
258
+
259
+
260
+ def dumps_wire(value: JsonValue) -> str:
261
+ """Serialize *value* the way the platform writes protocol and HTTP payloads: compact."""
262
+ out: list[str] = []
263
+ _write(value, out, None, 0)
264
+ return "".join(out)
265
+
266
+
267
+ def encode_wire(value: JsonValue) -> bytes:
268
+ """UTF-8 bytes of :func:`dumps_wire`."""
269
+ return dumps_wire(value).encode("utf-8")
270
+
271
+
272
+ def loads(payload: str | bytes | bytearray) -> JsonValue:
273
+ """Parse JSON, keeping number literals intact and rejecting documents that are too deep.
274
+
275
+ A UTF-8 byte order mark is tolerated because editors on Windows add one, which is what
276
+ ``ContractDocumentReader`` does too.
277
+ """
278
+ text = bytes(payload).decode("utf-8-sig") if isinstance(payload, (bytes, bytearray)) else payload.lstrip(_BOM)
279
+
280
+ value = json.loads(text, parse_int=RawInt, parse_float=RawFloat)
281
+ _check_depth(value, 0)
282
+ return value
283
+
284
+
285
+ def _check_depth(value: JsonValue, depth: int) -> None:
286
+ if depth > MAXIMUM_DEPTH:
287
+ raise ValueError(f"The document is nested deeper than {MAXIMUM_DEPTH} levels.")
288
+
289
+ for child in _children(value):
290
+ _check_depth(child, depth + 1)
291
+
292
+
293
+ def _children(value: JsonValue) -> Iterator[JsonValue]:
294
+ if isinstance(value, Mapping):
295
+ yield from value.values()
296
+ elif isinstance(value, list):
297
+ yield from value
@@ -0,0 +1,130 @@
1
+ """Run an agent definition: a model that calls tools until it has an answer.
2
+
3
+ Needs the ``agents`` extra (``velophex[agents]``). The loop is LangGraph, but nothing here
4
+ exposes it: a definition goes in (``agent-definition`` v1), a validated output comes out, and
5
+ every limit, tool call, approval and trace span goes through the platform's own layer.
6
+
7
+ >>> import velophex
8
+ >>> from velophex import agents
9
+ >>>
10
+ >>> @velophex.entrypoint(id="agent")
11
+ ... def run(question: str) -> dict:
12
+ ... return agents.run(DEFINITION, {"question": question})
13
+
14
+ Inside a job the model is called through the Orchestrator's LLM gateway with the job token,
15
+ tools reach the platform under the same token (process tools run in this job, through the
16
+ Robot), and each step is traced. Outside one (``velophex run``) ``VELOPHEX_AI_BASE_URL``,
17
+ ``VELOPHEX_AI_API_KEY`` and optionally ``VELOPHEX_AI_MODEL`` name an OpenAI-compatible
18
+ endpoint, and nothing is traced.
19
+
20
+ Defaults: 12 model calls, 50 000 tokens and 15 minutes per run, temperature 0.2. A run that
21
+ reaches a limit fails with ``agent_limit_exceeded``; an answer that does not satisfy the output
22
+ schema gets one repair turn, then fails with ``agent_output_invalid``. A run that fails after a
23
+ tool call succeeded is never retried automatically.
24
+ """
25
+
26
+ from __future__ import annotations
27
+
28
+ import os
29
+ import time
30
+ import uuid
31
+ from collections.abc import Mapping
32
+ from datetime import UTC, datetime
33
+ from typing import Any
34
+
35
+ from velophex.agents._errors import (
36
+ AgentDefinitionError,
37
+ AgentError,
38
+ AgentInputError,
39
+ AgentLimitExceededError,
40
+ AgentOutputInvalidError,
41
+ ToolOutcomeUnknownError,
42
+ )
43
+ from velophex.agents._loop import SUPPORTED_LANGGRAPH_MINOR, ChatEndpoint, run_definition
44
+ from velophex.agents._tools import APPROVAL_TIMEOUT_SECONDS, RuntimeTools, ToolClient, ToolOutcome, fence
45
+ from velophex.agents._tracing import NullTracer, RuntimeTracer, Tracer
46
+ from velophex.contracts.agents import AgentDefinition, AgentTool
47
+ from velophex.runtime import current_session, runtime_base_url
48
+
49
+ __all__ = [
50
+ "APPROVAL_TIMEOUT_SECONDS",
51
+ "SUPPORTED_LANGGRAPH_MINOR",
52
+ "AgentDefinition",
53
+ "AgentDefinitionError",
54
+ "AgentError",
55
+ "AgentInputError",
56
+ "AgentLimitExceededError",
57
+ "AgentOutputInvalidError",
58
+ "AgentTool",
59
+ "ChatEndpoint",
60
+ "NullTracer",
61
+ "RuntimeTools",
62
+ "RuntimeTracer",
63
+ "ToolClient",
64
+ "ToolOutcome",
65
+ "ToolOutcomeUnknownError",
66
+ "Tracer",
67
+ "fence",
68
+ "run",
69
+ ]
70
+
71
+ #: Seconds kept back from the job's deadline, so the run ends on its own terms rather than killed.
72
+ _DEADLINE_MARGIN_SECONDS = 15
73
+
74
+
75
+ def run(
76
+ definition: AgentDefinition | Mapping[str, Any],
77
+ inputs: Mapping[str, Any] | None = None,
78
+ *,
79
+ chat: ChatEndpoint | None = None,
80
+ tools: ToolClient | None = None,
81
+ tracer: Tracer | None = None,
82
+ ) -> dict[str, Any]:
83
+ """Run ``definition`` on ``inputs`` and return its validated output.
84
+
85
+ ``chat``, ``tools`` and ``tracer`` default to the job's (the gateway, the Runtime API and
86
+ the Robot, the job's trace); a test passes the fakes in :mod:`velophex.agents.testing`.
87
+ """
88
+ spec = definition if isinstance(definition, AgentDefinition) else AgentDefinition.model_validate(dict(definition))
89
+ session = current_session()
90
+ context = session.context if session is not None else None
91
+ in_job = context is not None and bool(context.orchestrator_url) and bool(context.runtime_token)
92
+
93
+ deadline = time.monotonic() + spec.guardrails.timeout_seconds
94
+
95
+ if context is not None and context.deadline is not None:
96
+ left = (context.deadline - datetime.now(UTC)).total_seconds() - _DEADLINE_MARGIN_SECONDS
97
+ deadline = min(deadline, time.monotonic() + left)
98
+
99
+ if tracer is None:
100
+ tracer = RuntimeTracer(session.client()) if in_job and session is not None else NullTracer()
101
+
102
+ return run_definition(
103
+ spec,
104
+ dict(inputs or {}),
105
+ chat=chat or _chat_endpoint(),
106
+ tools=tools or RuntimeTools(),
107
+ tracer=tracer,
108
+ run_id=str(context.job_id) if context is not None and context.job_id else uuid.uuid4().hex,
109
+ deadline=deadline,
110
+ )
111
+
112
+
113
+ def _chat_endpoint() -> ChatEndpoint:
114
+ """The job's LLM gateway, or the local endpoint the environment names."""
115
+ session = current_session()
116
+ context = session.context if session is not None else None
117
+
118
+ if context is not None and context.orchestrator_url and context.runtime_token:
119
+ headers = {"traceparent": context.trace_parent} if context.trace_parent else {}
120
+ return ChatEndpoint(runtime_base_url(context.orchestrator_url) + "/ai", context.runtime_token, headers=headers)
121
+
122
+ base_url = os.environ.get("VELOPHEX_AI_BASE_URL")
123
+
124
+ if not base_url:
125
+ raise AgentError(
126
+ "There is no model to call: run the agent in a job, or set VELOPHEX_AI_BASE_URL "
127
+ "(and VELOPHEX_AI_API_KEY) to an OpenAI-compatible endpoint for a local run."
128
+ )
129
+
130
+ return ChatEndpoint(base_url, os.environ.get("VELOPHEX_AI_API_KEY") or "none", os.environ.get("VELOPHEX_AI_MODEL"))
@@ -0,0 +1,75 @@
1
+ """The errors an agent run ends with, each reported under its own fault code."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from velophex.contracts.executor import FAULT_ENVELOPE_VERSION, FaultInfo, FaultRetryScopes
6
+ from velophex.contracts.runtimes import FaultCodes, FaultKinds
7
+ from velophex.errors import VelophexError
8
+
9
+ __all__ = [
10
+ "AgentDefinitionError",
11
+ "AgentError",
12
+ "AgentInputError",
13
+ "AgentLimitExceededError",
14
+ "AgentOutputInvalidError",
15
+ "ToolOutcomeUnknownError",
16
+ ]
17
+
18
+
19
+ class AgentError(VelophexError):
20
+ """Base of the errors an agent run raises itself. Never retried automatically."""
21
+
22
+ code: str = FaultCodes.UNHANDLED_EXCEPTION
23
+ fix: str | None = None
24
+ retry_scope: str = FaultRetryScopes.RUN
25
+
26
+ def to_fault(self) -> FaultInfo:
27
+ """Reported under :attr:`code`, as an ``Application`` fault that is not retried."""
28
+ return FaultInfo(
29
+ kind=FaultKinds.APPLICATION,
30
+ code=self.code,
31
+ message=str(self),
32
+ retryable=False,
33
+ version=FAULT_ENVELOPE_VERSION,
34
+ fix=self.fix,
35
+ retry_scope=self.retry_scope,
36
+ )
37
+
38
+
39
+ class AgentDefinitionError(AgentError):
40
+ """The definition cannot run on this SDK (a member version 1 does not have, a duplicate tool)."""
41
+
42
+ code = FaultCodes.INVALID_PACKAGE
43
+ fix = "Correct the agent definition and create a new candidate."
44
+ retry_scope = FaultRetryScopes.NONE
45
+
46
+
47
+ class AgentInputError(AgentError):
48
+ """The run's inputs do not satisfy the definition's input schema."""
49
+
50
+ code = FaultCodes.INVALID_INPUT
51
+ fix = "Start the run with inputs that match the agent's input schema."
52
+ retry_scope = FaultRetryScopes.NONE
53
+
54
+
55
+ class AgentOutputInvalidError(AgentError):
56
+ """The answer did not satisfy the output schema, after one repair turn."""
57
+
58
+ code = FaultCodes.AGENT_OUTPUT_INVALID
59
+ fix = "Make the instructions or the output schema clearer, then run again."
60
+
61
+
62
+ class AgentLimitExceededError(AgentError):
63
+ """The run reached a guardrail: its steps, its tokens or its timeout."""
64
+
65
+ code = FaultCodes.AGENT_LIMIT_EXCEEDED
66
+ fix = "Raise the agent's limits, or narrow its task, then run again."
67
+
68
+
69
+ class ToolOutcomeUnknownError(AgentError):
70
+ """A tool call ended without a known outcome. It is reconciled by a person, never replayed."""
71
+
72
+ code = FaultCodes.TOOL_OUTCOME_UNKNOWN
73
+ fix = "Check the system the tool writes to before running the agent again."
74
+ retry_scope = FaultRetryScopes.NONE
75
+ effects_committed = True