cortex-agent-sdk 0.0.1__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 (41) hide show
  1. cortex_agent_sdk/__init__.py +7 -0
  2. cortex_agent_sdk/agent.py +321 -0
  3. cortex_agent_sdk/engine.py +150 -0
  4. cortex_agent_sdk/errores/__init__.py +2 -0
  5. cortex_agent_sdk/errores/catalogo.py +35 -0
  6. cortex_agent_sdk/errores/excepcion.py +19 -0
  7. cortex_agent_sdk/gateway.py +23 -0
  8. cortex_agent_sdk/google/__init__.py +1 -0
  9. cortex_agent_sdk/history/__init__.py +2 -0
  10. cortex_agent_sdk/history/models.py +145 -0
  11. cortex_agent_sdk/history/pipeline.py +53 -0
  12. cortex_agent_sdk/history/transform.py +17 -0
  13. cortex_agent_sdk/hooks.py +102 -0
  14. cortex_agent_sdk/immutable.py +54 -0
  15. cortex_agent_sdk/lifecycle.py +67 -0
  16. cortex_agent_sdk/openai/__init__.py +2 -0
  17. cortex_agent_sdk/openai/engine.py +304 -0
  18. cortex_agent_sdk/openai/options.py +19 -0
  19. cortex_agent_sdk/postgres/__init__.py +1 -0
  20. cortex_agent_sdk/postgres/store.py +468 -0
  21. cortex_agent_sdk/py.typed +1 -0
  22. cortex_agent_sdk/redis/__init__.py +1 -0
  23. cortex_agent_sdk/redis/scripts.py +65 -0
  24. cortex_agent_sdk/redis/store.py +293 -0
  25. cortex_agent_sdk/results.py +29 -0
  26. cortex_agent_sdk/runtime.py +39 -0
  27. cortex_agent_sdk/sessions/__init__.py +4 -0
  28. cortex_agent_sdk/sessions/codec.py +27 -0
  29. cortex_agent_sdk/sessions/lease.py +85 -0
  30. cortex_agent_sdk/sessions/memory.py +186 -0
  31. cortex_agent_sdk/sessions/models.py +70 -0
  32. cortex_agent_sdk/sessions/store.py +38 -0
  33. cortex_agent_sdk/tools/__init__.py +1 -0
  34. cortex_agent_sdk/tools/contracts.py +152 -0
  35. cortex_agent_sdk/tools/decorators.py +15 -0
  36. cortex_agent_sdk/tools/execution.py +181 -0
  37. cortex_agent_sdk/tools/models.py +57 -0
  38. cortex_agent_sdk-0.0.1.dist-info/METADATA +192 -0
  39. cortex_agent_sdk-0.0.1.dist-info/RECORD +41 -0
  40. cortex_agent_sdk-0.0.1.dist-info/WHEEL +4 -0
  41. cortex_agent_sdk-0.0.1.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1,7 @@
1
+ from importlib.metadata import version as _package_version
2
+
3
+ from cortex_agent_sdk.agent import Agent
4
+ from cortex_agent_sdk.results import AgentResult
5
+ from cortex_agent_sdk.tools.decorators import final_answer, tool
6
+
7
+ __version__ = _package_version("cortex-agent-sdk")
@@ -0,0 +1,321 @@
1
+ import asyncio
2
+ from collections.abc import Awaitable, Callable, Iterable
3
+ from functools import partial
4
+ from typing import Self
5
+ from uuid import uuid4
6
+
7
+ from pydantic import BaseModel
8
+
9
+ from cortex_agent_sdk.engine import EngineRequest, EngineResult, ModelEngine, ToolCall
10
+ from cortex_agent_sdk.errores import AppError, CodigoError
11
+ from cortex_agent_sdk.history.models import Turn
12
+ from cortex_agent_sdk.history.pipeline import HistoryPipeline
13
+ from cortex_agent_sdk.history.transform import HistoryTransform
14
+ from cortex_agent_sdk.hooks import AgentHooks, HookChain
15
+ from cortex_agent_sdk.lifecycle import AgentLifecycle
16
+ from cortex_agent_sdk.results import AgentExitReason, AgentResult
17
+ from cortex_agent_sdk.runtime import AgentOptions, RunState
18
+ from cortex_agent_sdk.sessions import MemorySessionStore, SessionRecord, SessionStore
19
+ from cortex_agent_sdk.sessions.lease import LeaseKeeper
20
+ from cortex_agent_sdk.tools.execution import ToolExecutor, ToolOutcome, ToolSet
21
+ from cortex_agent_sdk.tools.models import ToolBinding, ToolFunction
22
+
23
+
24
+ class Agent:
25
+ """Fachada async y dueño único del loop de tools."""
26
+
27
+ def __init__(
28
+ self,
29
+ engine: ModelEngine,
30
+ *,
31
+ instructions: str = "",
32
+ tools: Iterable[ToolFunction | ToolBinding] = (),
33
+ hooks: AgentHooks | None = None,
34
+ history_transform: HistoryTransform | None = None,
35
+ session_store: SessionStore | None = None,
36
+ options: AgentOptions | None = None,
37
+ own_engine: bool = True,
38
+ own_session_store: bool = False,
39
+ ) -> None:
40
+ self._engine = engine
41
+ self._instructions = instructions
42
+ self._static_tools = tuple(tools)
43
+ self._options = options or AgentOptions()
44
+ self._history = HistoryPipeline(history_transform, self._options.max_history_turns)
45
+ self._hooks = HookChain(hooks or AgentHooks(), self._options.hook_timeout_seconds)
46
+ self._session_store = session_store or MemorySessionStore()
47
+ self._own_engine = own_engine
48
+ self._own_session_store = session_store is None or own_session_store
49
+ self._lifecycle = AgentLifecycle()
50
+
51
+ async def __aenter__(self) -> Self:
52
+ await self._lifecycle.ensure_open()
53
+ return self
54
+
55
+ async def __aexit__(self, *_: object) -> None:
56
+ await self.aclose()
57
+
58
+ async def run(
59
+ self,
60
+ text: str,
61
+ *,
62
+ session_id: str | None = None,
63
+ tools: Iterable[ToolFunction | ToolBinding] = (),
64
+ response_model: type[BaseModel] | None = None,
65
+ instructions: str | None = None,
66
+ ) -> AgentResult:
67
+ async with self._lifecycle.run():
68
+ if not text:
69
+ raise AppError(CodigoError.CONFIG_INVALIDA, "text no puede estar vacío")
70
+
71
+ tool_set = ToolSet.build((*self._static_tools, *tuple(tools)))
72
+ run_instructions = self._instructions if instructions is None else instructions
73
+ if session_id is None:
74
+ state = RunState(
75
+ history=[Turn.user(text)],
76
+ record=None,
77
+ lease_keeper=None,
78
+ instructions=run_instructions,
79
+ )
80
+ return await self._run_loop(state, tool_set, response_model, None)
81
+
82
+ timeout = self._options.session_lock_timeout_seconds
83
+ async with self._session_store.acquire(session_id, timeout) as lease:
84
+ lease_keeper = LeaseKeeper(lease)
85
+ await lease_keeper.start()
86
+ try:
87
+ record = await lease_keeper.load()
88
+ if record is None:
89
+ record = SessionRecord.create(
90
+ session_id, self._engine.provider, self._engine.model
91
+ )
92
+ self._validate_record(record, session_id)
93
+ history = [*record.history, Turn.user(text)]
94
+ state = RunState(
95
+ history=history,
96
+ record=record,
97
+ lease_keeper=lease_keeper,
98
+ instructions=run_instructions,
99
+ )
100
+ return await self._run_loop(state, tool_set, response_model, session_id)
101
+ finally:
102
+ await lease_keeper.aclose()
103
+
104
+ async def reset_session(self, session_id: str) -> None:
105
+ async with self._lifecycle.run():
106
+ if not session_id:
107
+ raise AppError(CodigoError.CONFIG_INVALIDA, "session_id no puede estar vacío")
108
+ await self._session_store.reset(
109
+ session_id,
110
+ self._options.session_lock_timeout_seconds,
111
+ )
112
+
113
+ async def aclose(self) -> None:
114
+ await self._lifecycle.close(
115
+ self._close_resources,
116
+ self._options.shutdown_timeout_seconds,
117
+ )
118
+
119
+ async def _close_resources(self) -> None:
120
+ try:
121
+ if self._own_engine:
122
+ await self._engine.aclose()
123
+ finally:
124
+ if self._own_session_store:
125
+ await self._session_store.aclose()
126
+
127
+ async def _run_loop(
128
+ self,
129
+ state: RunState,
130
+ tool_set: ToolSet,
131
+ response_model: type[BaseModel] | None,
132
+ session_id: str | None,
133
+ ) -> AgentResult:
134
+ executor = ToolExecutor(tool_set, self._options.tool_timeout_seconds)
135
+ for step in range(1, self._options.max_steps + 1):
136
+ engine_result = await self._generate(state, tool_set, response_model, session_id)
137
+ self._record_engine_result(state, engine_result)
138
+
139
+ if not engine_result.tool_calls:
140
+ return await self._finish(state, AgentExitReason.COMPLETED, step)
141
+
142
+ self._validate_tool_batch(tool_set, engine_result.tool_calls)
143
+ await self._start_tool_calls(state, engine_result.tool_calls)
144
+ outcomes = await self._execute_tool_calls(state, executor, engine_result.tool_calls)
145
+ await self._finish_tool_calls(state)
146
+
147
+ if state.consecutive_tool_failures >= self._options.max_consecutive_tool_failures:
148
+ return await self._finish(state, AgentExitReason.TOOL_FAILURE_LIMIT, step)
149
+ all_final = outcomes and all(
150
+ outcome.final_answer and not outcome.failed for outcome in outcomes
151
+ )
152
+ if all_final:
153
+ state.last_text = "\n\n".join(outcome.output for outcome in outcomes)
154
+ return await self._finish(state, AgentExitReason.FINAL_ANSWER, step)
155
+
156
+ return await self._finish(state, AgentExitReason.MAX_STEPS, self._options.max_steps)
157
+
158
+ async def _generate(
159
+ self,
160
+ state: RunState,
161
+ tool_set: ToolSet,
162
+ response_model: type[BaseModel] | None,
163
+ session_id: str | None,
164
+ ) -> EngineResult:
165
+ history = await self._history.prepare(tuple(state.history), session_id)
166
+ request = EngineRequest(
167
+ instructions=state.instructions,
168
+ history=history,
169
+ tools=tool_set.specs,
170
+ response_model=response_model,
171
+ )
172
+ request = await self._hooks.run_before_model(request)
173
+ try:
174
+ async with asyncio.timeout(self._options.provider_timeout_seconds):
175
+ result = await self._with_lease(
176
+ state,
177
+ lambda: self._engine.generate(request),
178
+ )
179
+ except TimeoutError as error:
180
+ raise AppError(CodigoError.PROVIDER_TIMEOUT, "provider agotó el timeout") from error
181
+ result = await self._hooks.run_after_model(result)
182
+ if result.provider != self._engine.provider:
183
+ raise AppError(CodigoError.PROVIDER_RESPUESTA_INVALIDA, "provider efectivo inválido")
184
+ return result
185
+
186
+ def _record_engine_result(self, state: RunState, result: EngineResult) -> None:
187
+ state.history.append(result.turn)
188
+ state.usage = state.usage + result.usage
189
+ state.raw_responses.append(result.raw)
190
+ state.last_text = result.turn.text or state.last_text
191
+ state.structured = result.structured or state.structured
192
+ state.effective_model = result.model
193
+
194
+ async def _start_tool_calls(
195
+ self,
196
+ state: RunState,
197
+ calls: tuple[ToolCall, ...],
198
+ ) -> None:
199
+ if state.record is None:
200
+ return
201
+ call_ids = tuple(call.call_id for call in calls)
202
+ state.record = state.record.start_turn(uuid4().hex, call_ids, tuple(state.history))
203
+ await self._save_record(state)
204
+
205
+ async def _execute_tool_calls(
206
+ self,
207
+ state: RunState,
208
+ executor: ToolExecutor,
209
+ calls: tuple[ToolCall, ...],
210
+ ) -> tuple[ToolOutcome, ...]:
211
+ outcomes: list[ToolOutcome] = []
212
+ for original_call in calls:
213
+ if state.consecutive_tool_failures >= self._options.max_consecutive_tool_failures:
214
+ outcome = ToolOutcome.skipped(original_call)
215
+ await self._checkpoint_outcome(state, outcome)
216
+ outcomes.append(outcome)
217
+ continue
218
+ call = await self._hooks.run_before_tool(original_call)
219
+ if call.call_id != original_call.call_id or call.name != original_call.name:
220
+ raise AppError(CodigoError.HOOK_FALLO, "before_tool cambió identidad de la call")
221
+ original_outcome = await self._with_lease(
222
+ state,
223
+ partial(executor.execute, call),
224
+ )
225
+ try:
226
+ outcome = await self._hooks.run_after_tool(original_outcome)
227
+ except AppError:
228
+ await self._checkpoint_outcome(state, original_outcome)
229
+ raise
230
+ if outcome.call != call:
231
+ await self._checkpoint_outcome(state, original_outcome)
232
+ raise AppError(CodigoError.HOOK_FALLO, "after_tool cambió identidad de la call")
233
+ await self._checkpoint_outcome(state, outcome)
234
+ outcomes.append(outcome)
235
+ state.tool_calls += 1
236
+ if outcome.failed:
237
+ state.consecutive_tool_failures += 1
238
+ else:
239
+ state.consecutive_tool_failures = 0
240
+ return tuple(outcomes)
241
+
242
+ async def _checkpoint_outcome(self, state: RunState, outcome: ToolOutcome) -> None:
243
+ turn = Turn(role="tool", parts=(outcome.as_history_part(),))
244
+ state.history.append(turn)
245
+ if state.record is None:
246
+ return
247
+ history = tuple(state.history)
248
+ state.record = state.record.complete_call(outcome.call.call_id, history)
249
+ await self._save_record(state)
250
+
251
+ async def _finish_tool_calls(self, state: RunState) -> None:
252
+ if state.record is None:
253
+ return
254
+ history = self._history.window(tuple(state.history))
255
+ state.history = list(history)
256
+ state.record = state.record.finish_turn(history)
257
+ await self._save_record(state)
258
+
259
+ async def _finish(
260
+ self,
261
+ state: RunState,
262
+ reason: AgentExitReason,
263
+ steps: int,
264
+ ) -> AgentResult:
265
+ state.history = list(self._history.window(tuple(state.history)))
266
+ result = AgentResult(
267
+ text=state.last_text,
268
+ reason=reason,
269
+ usage=state.usage,
270
+ steps=steps,
271
+ tool_calls=state.tool_calls,
272
+ provider=self._engine.provider,
273
+ model=state.effective_model or self._engine.model,
274
+ history=tuple(state.history),
275
+ raw_responses=tuple(state.raw_responses),
276
+ structured=state.structured,
277
+ )
278
+ await self._hooks.run_turn_finished(result)
279
+ if state.record is not None:
280
+ state.record = state.record.finish_turn(tuple(state.history))
281
+ await self._save_record(state)
282
+ return result
283
+
284
+ def _validate_tool_batch(self, tool_set: ToolSet, calls: tuple[ToolCall, ...]) -> None:
285
+ final_calls = [call for call in calls if tool_set.is_final_answer(call.name)]
286
+ if final_calls and len(calls) > 1:
287
+ raise AppError(
288
+ CodigoError.PROVIDER_RESPUESTA_INVALIDA,
289
+ "provider mezcló una tool terminal con otras calls en el mismo paso",
290
+ )
291
+
292
+ async def _save_record(self, state: RunState) -> None:
293
+ if state.record is None or state.lease_keeper is None:
294
+ return
295
+ state.record = await state.lease_keeper.save(state.record)
296
+
297
+ async def _with_lease[T](
298
+ self,
299
+ state: RunState,
300
+ operation: Callable[[], Awaitable[T]],
301
+ ) -> T:
302
+ if state.lease_keeper is None:
303
+ return await operation()
304
+ return await state.lease_keeper.run(operation)
305
+
306
+ def _validate_record(self, record: SessionRecord, session_id: str) -> None:
307
+ if record.session_id != session_id:
308
+ raise AppError(
309
+ CodigoError.SESION_INVALIDA,
310
+ "el store devolvió una sesión con identidad distinta",
311
+ )
312
+ if record.active_turn is not None:
313
+ raise AppError(
314
+ CodigoError.SESION_RECUPERACION_REQUERIDA,
315
+ f"turno incompleto en sesión {record.session_id}",
316
+ )
317
+ if record.provider != self._engine.provider or record.model != self._engine.model:
318
+ raise AppError(
319
+ CodigoError.SESION_ENGINE_DISTINTO,
320
+ f"sesión ligada a {record.provider}/{record.model}",
321
+ )
@@ -0,0 +1,150 @@
1
+ from collections.abc import Mapping
2
+ from dataclasses import dataclass
3
+ from types import MappingProxyType
4
+ from typing import Protocol, cast, runtime_checkable
5
+
6
+ from pydantic import BaseModel, ConfigDict, JsonValue, field_serializer, field_validator
7
+
8
+ from cortex_agent_sdk.history.models import ToolCallPart, Turn
9
+ from cortex_agent_sdk.immutable import FrozenJsonValue, freeze_json, thaw_json
10
+ from cortex_agent_sdk.tools.models import ToolSpec
11
+
12
+
13
+ class ToolCall(BaseModel):
14
+ model_config = ConfigDict(extra="forbid", frozen=True)
15
+
16
+ call_id: str
17
+ name: str
18
+ arguments: Mapping[str, JsonValue]
19
+
20
+ @field_validator("arguments", mode="after")
21
+ @classmethod
22
+ def freeze_arguments(
23
+ cls,
24
+ value: Mapping[str, JsonValue],
25
+ ) -> Mapping[str, JsonValue]:
26
+ frozen = freeze_json(value)
27
+ if not isinstance(frozen, Mapping):
28
+ raise TypeError("arguments debe ser un objeto")
29
+ return cast(Mapping[str, JsonValue], frozen)
30
+
31
+ @field_serializer("arguments", when_used="json")
32
+ def serialize_arguments(
33
+ self,
34
+ value: Mapping[str, JsonValue],
35
+ ) -> dict[str, object]:
36
+ return {key: thaw_json(cast(FrozenJsonValue, item)) for key, item in value.items()}
37
+
38
+
39
+ @dataclass(frozen=True, slots=True)
40
+ class Usage:
41
+ input_tokens: int = 0
42
+ output_tokens: int = 0
43
+ reasoning_tokens: int = 0
44
+ cached_tokens: int = 0
45
+ cache_write_tokens: int = 0
46
+
47
+ def __post_init__(self) -> None:
48
+ if (
49
+ min(
50
+ self.input_tokens,
51
+ self.output_tokens,
52
+ self.reasoning_tokens,
53
+ self.cached_tokens,
54
+ self.cache_write_tokens,
55
+ )
56
+ < 0
57
+ ):
58
+ raise ValueError("usage no admite valores negativos")
59
+
60
+ def __add__(self, other: "Usage") -> "Usage":
61
+ return Usage(
62
+ input_tokens=self.input_tokens + other.input_tokens,
63
+ output_tokens=self.output_tokens + other.output_tokens,
64
+ reasoning_tokens=self.reasoning_tokens + other.reasoning_tokens,
65
+ cached_tokens=self.cached_tokens + other.cached_tokens,
66
+ cache_write_tokens=self.cache_write_tokens + other.cache_write_tokens,
67
+ )
68
+
69
+
70
+ @dataclass(frozen=True, slots=True)
71
+ class EngineRequest:
72
+ instructions: str
73
+ history: tuple[Turn, ...]
74
+ tools: tuple[ToolSpec, ...]
75
+ response_model: type[BaseModel] | None = None
76
+ metadata: Mapping[str, str] | None = None
77
+
78
+ def __post_init__(self) -> None:
79
+ valid_history = isinstance(self.history, tuple) and all(
80
+ isinstance(turn, Turn) for turn in self.history
81
+ )
82
+ valid_tools = isinstance(self.tools, tuple) and all(
83
+ isinstance(spec, ToolSpec) for spec in self.tools
84
+ )
85
+ valid_model = self.response_model is None or (
86
+ isinstance(self.response_model, type) and issubclass(self.response_model, BaseModel)
87
+ )
88
+ if (
89
+ not isinstance(self.instructions, str)
90
+ or not valid_history
91
+ or not valid_tools
92
+ or not valid_model
93
+ ):
94
+ raise TypeError("EngineRequest inválido")
95
+ if self.metadata is not None:
96
+ if not all(
97
+ isinstance(key, str) and isinstance(value, str)
98
+ for key, value in self.metadata.items()
99
+ ):
100
+ raise TypeError("metadata inválida")
101
+ object.__setattr__(self, "metadata", MappingProxyType(dict(self.metadata)))
102
+
103
+
104
+ @dataclass(frozen=True, slots=True)
105
+ class EngineResult:
106
+ turn: Turn
107
+ tool_calls: tuple[ToolCall, ...]
108
+ usage: Usage
109
+ provider: str
110
+ model: str
111
+ stop_reason: str | None
112
+ raw: object
113
+ structured: BaseModel | None = None
114
+
115
+ def __post_init__(self) -> None:
116
+ valid_calls = isinstance(self.tool_calls, tuple) and all(
117
+ isinstance(call, ToolCall) for call in self.tool_calls
118
+ )
119
+ valid_structured = self.structured is None or isinstance(self.structured, BaseModel)
120
+ if not isinstance(self.turn, Turn) or not valid_calls:
121
+ raise TypeError("EngineResult inválido")
122
+ turn_calls = tuple(part for part in self.turn.parts if isinstance(part, ToolCallPart))
123
+ aligned_calls = len(turn_calls) == len(self.tool_calls) and all(
124
+ part.call_id == call.call_id
125
+ and part.name == call.name
126
+ and part.arguments == call.arguments
127
+ for part, call in zip(turn_calls, self.tool_calls, strict=True)
128
+ )
129
+ if not aligned_calls:
130
+ raise TypeError("tool_calls no coincide con el turno")
131
+ if not isinstance(self.usage, Usage) or not self.provider or not self.model:
132
+ raise TypeError("EngineResult inválido")
133
+ if self.stop_reason is not None and not isinstance(self.stop_reason, str):
134
+ raise TypeError("stop_reason inválido")
135
+ if not valid_structured:
136
+ raise TypeError("structured inválido")
137
+
138
+
139
+ @runtime_checkable
140
+ class ModelEngine(Protocol):
141
+ @property
142
+ def provider(self) -> str: ...
143
+
144
+ @property
145
+ def model(self) -> str: ...
146
+
147
+ async def generate(self, request: EngineRequest) -> EngineResult: ...
148
+
149
+ async def aclose(self) -> None: ...
150
+
@@ -0,0 +1,2 @@
1
+ from cortex_agent_sdk.errores.catalogo import CodigoError, Severidad
2
+ from cortex_agent_sdk.errores.excepcion import AppError
@@ -0,0 +1,35 @@
1
+ from enum import StrEnum
2
+
3
+
4
+ class Severidad(StrEnum):
5
+ INFO = "INFO"
6
+ WARNING = "WARNING"
7
+ ERROR = "ERROR"
8
+ CRITICAL = "CRITICAL"
9
+
10
+
11
+ class CodigoError(StrEnum):
12
+ UNKNOWN = "UNKNOWN"
13
+ CONFIG_INVALIDA = "CONFIG_INVALIDA"
14
+ RECURSO_CERRADO = "RECURSO_CERRADO"
15
+ RUNTIME_CIERRE_TIMEOUT = "RUNTIME_CIERRE_TIMEOUT"
16
+ PROVIDER_FALLO = "PROVIDER_FALLO"
17
+ PROVIDER_TIMEOUT = "PROVIDER_TIMEOUT"
18
+ PROVIDER_RESPUESTA_INVALIDA = "PROVIDER_RESPUESTA_INVALIDA"
19
+ TOOL_NO_ENCONTRADA = "TOOL_NO_ENCONTRADA"
20
+ TOOL_ARGUMENTOS_INVALIDOS = "TOOL_ARGUMENTOS_INVALIDOS"
21
+ TOOL_TIMEOUT = "TOOL_TIMEOUT"
22
+ TOOL_FALLO = "TOOL_FALLO"
23
+ TOOL_OMITIDA_POR_LIMITE = "TOOL_OMITIDA_POR_LIMITE"
24
+ TOOL_RESULTADO_INVALIDO = "TOOL_RESULTADO_INVALIDO"
25
+ FINAL_ANSWER_INVALIDA = "FINAL_ANSWER_INVALIDA"
26
+ HOOK_FALLO = "HOOK_FALLO"
27
+ HISTORIAL_TRANSFORM_INVALIDO = "HISTORIAL_TRANSFORM_INVALIDO"
28
+ SESION_OCUPADA = "SESION_OCUPADA"
29
+ SESION_LEASE_PERDIDO = "SESION_LEASE_PERDIDO"
30
+ SESION_CONFLICTO = "SESION_CONFLICTO"
31
+ SESION_ENGINE_DISTINTO = "SESION_ENGINE_DISTINTO"
32
+ SESION_RECUPERACION_REQUERIDA = "SESION_RECUPERACION_REQUERIDA"
33
+ SESION_INVALIDA = "SESION_INVALIDA"
34
+ SESION_STORE_FALLO = "SESION_STORE_FALLO"
35
+
@@ -0,0 +1,19 @@
1
+ from cortex_agent_sdk.errores.catalogo import CodigoError, Severidad
2
+
3
+
4
+ class AppError(Exception):
5
+ """Error del SDK con código estable y mensaje seguro opcional."""
6
+
7
+ def __init__(
8
+ self,
9
+ codigo: CodigoError,
10
+ detalle: str,
11
+ severidad: Severidad = Severidad.ERROR,
12
+ mensaje_seguro: str | None = None,
13
+ ) -> None:
14
+ self.codigo = codigo
15
+ self.detalle = detalle
16
+ self.severidad = severidad
17
+ self.mensaje_seguro = mensaje_seguro
18
+ super().__init__(f"[{codigo.value}] {detalle}")
19
+
@@ -0,0 +1,23 @@
1
+ from pydantic import AnyHttpUrl, BaseModel, ConfigDict, Field, SecretStr
2
+
3
+
4
+ class TalosGateway(BaseModel):
5
+ """Conexión interna a Talos para un engine nativo."""
6
+
7
+ model_config = ConfigDict(extra="forbid", frozen=True)
8
+
9
+ url: AnyHttpUrl
10
+ project_token: SecretStr = Field(min_length=32)
11
+
12
+ def __init__(
13
+ self,
14
+ *,
15
+ url: str | AnyHttpUrl,
16
+ project_token: str | SecretStr,
17
+ ) -> None:
18
+ super().__init__(url=url, project_token=project_token)
19
+
20
+ @property
21
+ def openai_base_url(self) -> str:
22
+ return f"{str(self.url).rstrip('/')}/v1"
23
+
@@ -0,0 +1 @@
1
+ """Espacio reservado para el engine de Google posterior a 0.0.1."""
@@ -0,0 +1,2 @@
1
+ from cortex_agent_sdk.history.models import TextPart, ToolCallPart, ToolResultPart, Turn
2
+ from cortex_agent_sdk.history.transform import TransformContext