ygo74-agent-runtime-agentframework 0.8.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.
@@ -0,0 +1,544 @@
1
+ """Native MAF execution with one lifecycle driver for both response modes."""
2
+
3
+ import logging
4
+ from collections.abc import AsyncGenerator, Callable
5
+ from contextlib import aclosing
6
+ from dataclasses import dataclass, replace
7
+ from typing import Any
8
+ from uuid import uuid4
9
+
10
+ from agent_framework import (
11
+ Agent,
12
+ AgentResponse,
13
+ AgentSession,
14
+ BaseChatClient,
15
+ Message,
16
+ SupportsAgentRun,
17
+ )
18
+ from ygo74.agent_runtime.domains.contracts.agent_output import (
19
+ AgentOutput,
20
+ Termination,
21
+ TerminationStatus,
22
+ TextContent,
23
+ TokenUsage,
24
+ ToolCallContent,
25
+ ToolResultContent,
26
+ )
27
+ from ygo74.agent_runtime.domains.contracts.error_envelope import ErrorEnvelope
28
+ from ygo74.agent_runtime.domains.contracts.stream_events import (
29
+ AgentStreamEvent,
30
+ ContentEnd,
31
+ ContentEvent,
32
+ ContentStart,
33
+ TerminalEvent,
34
+ ToolArgumentsDelta,
35
+ UsageEvent,
36
+ )
37
+ from ygo74.agent_runtime.domains.humanapproval.approval_loop import (
38
+ DEFAULT_EXHAUSTED_MESSAGE,
39
+ DEFAULT_INTERRUPTED_MESSAGE,
40
+ ApprovalBudget,
41
+ ApprovalLoop,
42
+ )
43
+ from ygo74.agent_runtime.domains.sessions.continuity_diagnostics import (
44
+ ContinuationReference,
45
+ ContinuityDiagnostics,
46
+ )
47
+
48
+ from .approval import ApprovalResolver, MafApprovalTranslator, PendingToolApproval
49
+ from .output_adapter import AgentFrameworkOutputAdapter
50
+ from .progress import ConversionDiagnostics, NativeStreamScope, ToolProgressPolicy
51
+ from .stream_adapter import AgentFrameworkStreamAdapter
52
+
53
+ _logger = logging.getLogger(__name__)
54
+
55
+
56
+ @dataclass(frozen=True, slots=True)
57
+ class _Round:
58
+ """One native sub-run result.
59
+
60
+ Args:
61
+ response: Native state used by the optional approval bridge.
62
+ output: Converted safe output and real reported usage.
63
+ local_approval: SDK-only approval dispatch with no provider invocation.
64
+ """
65
+
66
+ response: AgentResponse[Any]
67
+ output: AgentOutput
68
+ local_approval: bool
69
+
70
+
71
+ class UsageAccumulator:
72
+ """Sum real sub-run counts, preserving unknown optional counters."""
73
+
74
+ def __init__(self) -> None:
75
+ """Initialize an invocation with no reported usage."""
76
+ self.usage: TokenUsage | None = None
77
+ self._unreported = False
78
+
79
+ def add(self, usage: TokenUsage | None) -> None:
80
+ """Add each sub-run exactly once.
81
+
82
+ Args:
83
+ usage: Final native snapshot, or last update snapshot if absent.
84
+ """
85
+ if usage is None:
86
+ if self.usage is not None:
87
+ raise RuntimeError("Native sub-runs must report usage consistently")
88
+ self._unreported = True
89
+ return
90
+ if self._unreported:
91
+ raise RuntimeError("Native sub-runs must report usage consistently")
92
+ if self.usage is None:
93
+ self.usage = usage
94
+ return
95
+ previous = self.usage
96
+ optional = (
97
+ "total_tokens", "cached_input_tokens", "reasoning_output_tokens", "cache_write_input_tokens",
98
+ )
99
+ counts = [None if getattr(previous, key) is None or getattr(usage, key) is None
100
+ else getattr(previous, key) + getattr(usage, key) for key in optional]
101
+ self.usage = TokenUsage(previous.input_tokens + usage.input_tokens,
102
+ previous.output_tokens + usage.output_tokens, *counts)
103
+
104
+
105
+ class AgentFrameworkSession:
106
+ """Bind an existing native Agent; no skills, registry or HTTP knowledge needed.
107
+
108
+ Args:
109
+ agent: Existing MAF agent, never reconstructed by the binding.
110
+ session_id: Conversation history identifier.
111
+ resolver: Optional advanced approval resolver.
112
+ translator: Optional native approval translator.
113
+ discard_authorizations: Domain grant cleanup for error/cancellation.
114
+ tool_names: Admitted tool names eligible for safe activity.
115
+ max_approval_rounds: Maximum actual human-question batches.
116
+ max_total_rounds: Maximum approval batches.
117
+ interrupted_message: Optional domain wording for a question-budget stop.
118
+ exhausted_message: Optional domain wording for a total-budget stop.
119
+ require_provider_continuation: Fail closed when provider-only history is unavailable.
120
+ """
121
+
122
+ def __init__(
123
+ self, agent: SupportsAgentRun, *, session_id: str | None = None,
124
+ resolver: ApprovalResolver | None = None, translator: MafApprovalTranslator | None = None,
125
+ discard_authorizations: Callable[[], None] | None = None, tool_names: tuple[str, ...] = (),
126
+ max_approval_rounds: int = 25, max_total_rounds: int = 200,
127
+ interrupted_message: str | None = None, exhausted_message: str | None = None,
128
+ require_provider_continuation: bool = False,
129
+ ) -> None:
130
+ """Create a native session and the shared approval driver.
131
+
132
+ Args:
133
+ agent: Native agent to execute.
134
+ session_id: Native conversation identifier.
135
+ resolver: Optional advanced resolver.
136
+ translator: Optional bridge translator.
137
+ discard_authorizations: Deterministic fail-closed grant cleanup.
138
+ tool_names: Trusted public activity names.
139
+ max_approval_rounds: Prompt bound.
140
+ max_total_rounds: Resume bound.
141
+ interrupted_message: Optional prompt-limit explanation.
142
+ exhausted_message: Optional resume-limit explanation.
143
+ require_provider_continuation: Explicit provider-only history profile; never enables local fallback.
144
+ """
145
+ self._agent = agent
146
+ self._require_provider_continuation = require_provider_continuation
147
+ if require_provider_continuation and isinstance(agent, Agent) and agent.default_options.get("store") is False:
148
+ raise ValueError("Provider history profile conflicts with local storage")
149
+ self._instance = uuid4().hex
150
+ self._turn = 0
151
+ self._subrun = 0
152
+ if isinstance(agent, Agent):
153
+ NativeStreamScope.attach(agent.client)
154
+ self._session: AgentSession = agent.create_session(session_id=session_id)
155
+ self._diagnose("create")
156
+ self._resolver = resolver
157
+ self._translator = translator or MafApprovalTranslator()
158
+ self._discard = discard_authorizations
159
+ self._tool_names = tool_names
160
+ self._broken = False
161
+ self._cleanup_usage: list[TokenUsage | None] = []
162
+ self._cleanup_failure: Termination | None = None
163
+ self._loop = ApprovalLoop(self, max_approval_rounds=max_approval_rounds,
164
+ max_total_rounds=max_total_rounds,
165
+ interrupted_message=interrupted_message or DEFAULT_INTERRUPTED_MESSAGE,
166
+ exhausted_message=exhausted_message or DEFAULT_EXHAUSTED_MESSAGE)
167
+
168
+ async def ask(self, message: str) -> str:
169
+ """Answer a normal native run.
170
+
171
+ Args:
172
+ message: Latest user text; history remains server-managed.
173
+ """
174
+ output = await self.ask_output(message)
175
+ if output.termination.status is TerminationStatus.FAILED:
176
+ raise RuntimeError("Native agent execution failed")
177
+ return "".join(item.text for item in output.contents if isinstance(item, TextContent))
178
+
179
+ async def ask_output(self, message: str) -> AgentOutput:
180
+ """Return typed output, preserving native termination and usage.
181
+
182
+ Args:
183
+ message: Latest user text.
184
+ """
185
+ async with aclosing(self._execute(message, stream=False)) as execution:
186
+ async for item in execution:
187
+ if isinstance(item, AgentOutput):
188
+ return item
189
+ raise RuntimeError("native execution produced no output")
190
+
191
+ async def ask_stream(
192
+ self, message: str, *, require_initial_usage: bool = False,
193
+ ) -> AsyncGenerator[AgentStreamEvent]:
194
+ """Stream real native updates through existing adapters.
195
+
196
+ Args:
197
+ message: Latest user text.
198
+ require_initial_usage: Hold visible events until this stream reports real token counts.
199
+ """
200
+ async with aclosing(self._execute(
201
+ message, stream=True, require_initial_usage=require_initial_usage,
202
+ )) as execution:
203
+ async for item in execution:
204
+ if not isinstance(item, AgentOutput):
205
+ yield item
206
+
207
+ async def _execute(
208
+ self, message: str, *, stream: bool, require_initial_usage: bool = False,
209
+ ) -> AsyncGenerator[AgentStreamEvent | AgentOutput]:
210
+ """Own native sub-runs and exactly one invocation terminal.
211
+
212
+ Args:
213
+ message: Initial user text.
214
+ stream: Native response mode, not an output simulation flag.
215
+ require_initial_usage: Hold visible events until actual usage is available.
216
+ """
217
+ if self._broken:
218
+ raise RuntimeError("native session was invalidated; create a fresh conversation")
219
+ budget, usage = ApprovalBudget(), UsageAccumulator()
220
+ self._turn += 1
221
+ self._subrun = 0
222
+ self._cleanup_usage.clear()
223
+ self._cleanup_failure = None
224
+ current: str | Message = message
225
+ completed = False
226
+ last_usage: TokenUsage | None = None
227
+ try:
228
+ while True:
229
+ result: _Round | None = None
230
+ async with aclosing(self._round(
231
+ current,
232
+ stream=stream,
233
+ previous_usage=usage.usage,
234
+ require_initial_usage=require_initial_usage,
235
+ )) as round_stream:
236
+ async for item in round_stream:
237
+ if isinstance(item, _Round):
238
+ result = item
239
+ else:
240
+ if isinstance(item, UsageEvent):
241
+ last_usage = item.usage
242
+ yield item
243
+ if result is None:
244
+ raise RuntimeError("native sub-run produced no response")
245
+ if not result.local_approval:
246
+ usage.add(result.output.usage)
247
+ output = replace(result.output, usage=usage.usage)
248
+ if output.termination.status is TerminationStatus.FAILED:
249
+ self.discard_authorizations()
250
+ self._broken = True
251
+ self._diagnose("invalidate", reason="native_failed")
252
+ break
253
+ step = await self._loop.advance(result.response, budget)
254
+ if step.interruption is not None:
255
+ try:
256
+ for cleanup_usage in self._cleanup_usage:
257
+ usage.add(cleanup_usage)
258
+ except RuntimeError:
259
+ if self._cleanup_failure is None:
260
+ raise
261
+ # Missing refusal counts cannot replace a native failure
262
+ # with a usage error or make a partial total authoritative.
263
+ usage = UsageAccumulator()
264
+ if self._cleanup_failure is not None:
265
+ output = AgentOutput((), usage.usage, self._cleanup_failure)
266
+ break
267
+ # An interrupted CLI conversation may continue, but must never
268
+ # reuse unresolved SDK approval state after best-effort cleanup.
269
+ self._session = self._agent.create_session(session_id=self._session.session_id)
270
+ self._diagnose("recreate", reason="approval_budget")
271
+ output = AgentOutput((TextContent(step.interruption),), usage.usage,
272
+ Termination(TerminationStatus.INCOMPLETE, "approval_budget"))
273
+ if stream:
274
+ yield ContentEvent(uuid4().hex, TextContent(step.interruption))
275
+ break
276
+ if not step.pending:
277
+ break
278
+ if self._resolver is None:
279
+ raise RuntimeError("native approvals require an explicit approval bridge")
280
+ answers = await self._resolver.resolve(step.pending)
281
+ current = self._translator.answer_message(answers.answers)
282
+ if stream:
283
+ if usage.usage is not None and usage.usage != last_usage:
284
+ yield UsageEvent(usage.usage)
285
+ completed = True
286
+ yield TerminalEvent(output.termination)
287
+ else:
288
+ completed = True
289
+ yield output
290
+ finally:
291
+ if not completed:
292
+ self._broken = True
293
+ self._diagnose("invalidate", reason="error_or_cancel")
294
+ self.discard_authorizations()
295
+
296
+ async def _round(
297
+ self, message: str | Message, *, stream: bool, previous_usage: TokenUsage | None,
298
+ require_initial_usage: bool = False,
299
+ ) -> AsyncGenerator[AgentStreamEvent | _Round]:
300
+ """Convert one sub-run and close producer/content lifecycle on exit.
301
+
302
+ Args:
303
+ message: User text or native approval answers.
304
+ stream: Whether to invoke native streaming.
305
+ previous_usage: Completed sub-run counters, for cumulative snapshots.
306
+ require_initial_usage: Hold visible events until actual usage is available.
307
+ """
308
+ invocation_id = uuid4().hex
309
+ self._subrun += 1
310
+ self._diagnose("before", stream=stream)
311
+ adapter = AgentFrameworkStreamAdapter()
312
+ progress = ToolProgressPolicy(self._tool_names)
313
+ snapshot: TokenUsage | None = None
314
+ pending_events: list[AgentStreamEvent] = []
315
+ usage_emitted = False
316
+ hidden: set[str] = set()
317
+ terminal: Termination | None = None
318
+ scope = NativeStreamScope(
319
+ session_instance=self._instance, turn=self._turn, subrun=self._subrun,
320
+ storage_default=bool(getattr(getattr(self._agent, "client", None), "STORES_BY_DEFAULT", False)),
321
+ )
322
+ if stream:
323
+ with scope.activate():
324
+ producer = self._agent.run(message, stream=True, session=self._session)
325
+ try:
326
+ async for update in scope.updates(producer):
327
+ for progress_event in progress.observe(update):
328
+ if not require_initial_usage or usage_emitted:
329
+ yield progress_event
330
+ else:
331
+ pending_events.append(progress_event)
332
+ conversion = adapter.convert_update(update)
333
+ ConversionDiagnostics.record(conversion.decisions, invocation_id)
334
+ for event in conversion.events:
335
+ if isinstance(event, UsageEvent):
336
+ snapshot = event.usage
337
+ cumulative = UsageAccumulator()
338
+ if previous_usage is not None:
339
+ cumulative.add(previous_usage)
340
+ cumulative.add(snapshot)
341
+ if cumulative.usage is not None:
342
+ yield UsageEvent(cumulative.usage)
343
+ if require_initial_usage:
344
+ usage_emitted = True
345
+ for pending in pending_events:
346
+ yield pending
347
+ pending_events.clear()
348
+ elif self._visible(event, hidden):
349
+ projected = self._namespace(event, invocation_id)
350
+ if not require_initial_usage or usage_emitted:
351
+ yield projected
352
+ else:
353
+ pending_events.append(projected)
354
+ with scope.activate():
355
+ response = await producer.get_final_response()
356
+ for event in adapter.finish():
357
+ if isinstance(event, TerminalEvent):
358
+ terminal = event.termination
359
+ elif self._visible(event, hidden):
360
+ projected = self._namespace(event, invocation_id)
361
+ if not require_initial_usage or usage_emitted:
362
+ yield projected
363
+ else:
364
+ pending_events.append(projected)
365
+ except Exception as error: # Translate the native execution boundary without exposing provider messages.
366
+ raise RuntimeError("Native agent execution failed") from error
367
+ finally:
368
+ await scope.close(producer)
369
+ else:
370
+ try:
371
+ with scope.activate():
372
+ response = await self._agent.run(message, session=self._session)
373
+ except Exception as error: # Translate the native execution boundary without exposing provider messages.
374
+ raise RuntimeError("Native agent execution failed") from error
375
+ self._diagnose("after", stream=stream)
376
+ self._validate_continuity(scope)
377
+ conversion_output = AgentFrameworkOutputAdapter().convert(response)
378
+ ConversionDiagnostics.record(conversion_output.decisions, invocation_id)
379
+ output = conversion_output.output
380
+ if stream and require_initial_usage and pending_events and not usage_emitted:
381
+ round_usage = output.usage or snapshot
382
+ if round_usage is not None:
383
+ cumulative = UsageAccumulator()
384
+ if previous_usage is not None:
385
+ cumulative.add(previous_usage)
386
+ cumulative.add(round_usage)
387
+ if cumulative.usage is not None:
388
+ yield UsageEvent(cumulative.usage)
389
+ usage_emitted = True
390
+ for pending in pending_events:
391
+ yield pending
392
+ termination = output.termination
393
+ if terminal is not None and terminal.status is not TerminationStatus.SUCCESS:
394
+ termination = terminal
395
+ if termination.error is not None:
396
+ termination = replace(termination, error=ErrorEnvelope(
397
+ code="native_execution_failed", category="handler_execution", message="Native agent execution failed",
398
+ ))
399
+ contents = tuple(item for item in output.contents if not isinstance(item, (ToolCallContent, ToolResultContent)))
400
+ local_approval = (
401
+ isinstance(getattr(self._agent, "client", None), BaseChatClient)
402
+ and not scope.provider_called and output.usage is None and snapshot is None
403
+ and bool(response.messages)
404
+ and all(message.contents for message in response.messages)
405
+ and all(content.type == "function_approval_request"
406
+ for message in response.messages for content in message.contents)
407
+ )
408
+ # SDK middleware dispatches parallel approvals one at a time without
409
+ # inference. These rounds have no missing provider usage to accumulate.
410
+ yield _Round(response, AgentOutput(contents, output.usage or snapshot, termination), local_approval)
411
+
412
+ def _validate_continuity(self, scope: NativeStreamScope) -> None:
413
+ """Fail closed only for the explicitly selected provider-history profile.
414
+
415
+ Args:
416
+ scope: Finalized provider subcall observations; missing handles cannot reuse stale state.
417
+ """
418
+ if not self._require_provider_continuation:
419
+ return
420
+ ref = ContinuationReference.of(self._session.service_session_id)
421
+ if scope.missing_continuation or ref.fingerprint is None:
422
+ self._diagnose("missing_continuation")
423
+ raise RuntimeError("Provider history continuation is unavailable; create a fresh conversation")
424
+ if scope.last_continuation is not None and scope.last_continuation != ref:
425
+ self._diagnose("continuation_mismatch")
426
+ raise RuntimeError("Provider history continuation was not retained; create a fresh conversation")
427
+
428
+ def _diagnose(self, phase: str, *, stream: bool | None = None, reason: str | None = None) -> None:
429
+ """Record session continuity without exposing IDs or native state.
430
+
431
+ Args:
432
+ phase: Lifecycle stage.
433
+ stream: Native response mode when known.
434
+ reason: Trusted lifecycle reason.
435
+ """
436
+ ref = ContinuationReference.of(self._session.service_session_id)
437
+ ContinuityDiagnostics.record(
438
+ _logger, "native session continuity", phase=phase, reason=reason,
439
+ session_instance=self._instance, turn=self._turn, subrun=self._subrun, stream=stream,
440
+ conversation_fingerprint=ContinuityDiagnostics.fingerprint(self._session.session_id),
441
+ service_continuation_kind=ref.kind, service_continuation_fingerprint=ref.fingerprint,
442
+ require_provider_continuation=self._require_provider_continuation,
443
+ storage_explicit=(self._agent.default_options.get("store") if isinstance(self._agent, Agent) else None),
444
+ storage_default=bool(getattr(getattr(self._agent, "client", None), "STORES_BY_DEFAULT", False)),
445
+ )
446
+
447
+ @staticmethod
448
+ def _namespace(event: AgentStreamEvent, run_id: str) -> AgentStreamEvent:
449
+ """Prevent SDK-default content IDs from colliding across approval sub-runs.
450
+
451
+ Args:
452
+ event: Visible content/lifecycle event.
453
+ run_id: Unique native sub-run identifier, also used in diagnostics.
454
+ """
455
+ if isinstance(event, (UsageEvent, TerminalEvent)):
456
+ return event
457
+ return replace(event, content_id=f"{run_id}:{event.content_id}")
458
+
459
+ @staticmethod
460
+ def _visible(event: AgentStreamEvent, hidden: set[str]) -> bool:
461
+ """Filter entire internal tool lifecycles rather than leaking payloads.
462
+
463
+ Args:
464
+ event: Converted native event.
465
+ hidden: Tool content identities suppressed in this sub-run.
466
+ """
467
+ if isinstance(event, (ContentStart, ContentEvent)) and isinstance(event.content, (ToolCallContent, ToolResultContent)):
468
+ hidden.add(event.content_id)
469
+ return False
470
+ if isinstance(event, ToolArgumentsDelta):
471
+ return False
472
+ return not (isinstance(event, ContentEnd) and event.content_id in hidden)
473
+
474
+ def pending(self, state: AgentResponse[Any]) -> tuple[PendingToolApproval, ...]:
475
+ """Inspect native suspended calls.
476
+
477
+ Args:
478
+ state: Completed native response.
479
+ """
480
+ return self._translator.pending_approvals(state)
481
+
482
+ def will_question(self, pending: tuple[PendingToolApproval, ...]) -> bool:
483
+ """Classify human prompts using the injected resolver.
484
+
485
+ Args:
486
+ pending: Suspended batch.
487
+ """
488
+ return self._resolver is not None and self._resolver.will_question(pending)
489
+
490
+ async def resume(self, pending: tuple[PendingToolApproval, ...]) -> AgentResponse[Any]:
491
+ """Support the existing neutral normal-mode ApprovalLoop port.
492
+
493
+ Args:
494
+ pending: Suspended calls.
495
+ """
496
+ if self._resolver is None:
497
+ raise RuntimeError("native approvals require an explicit approval bridge")
498
+ answers = await self._resolver.resolve(pending)
499
+ return await self._agent.run(self._translator.answer_message(answers.answers), session=self._session)
500
+
501
+ async def decline(self, pending: tuple[PendingToolApproval, ...]) -> AgentResponse[Any]:
502
+ """Explicitly decline outstanding calls during bounded abandonment.
503
+
504
+ Args:
505
+ pending: Calls that must never execute.
506
+ """
507
+ answers = tuple(item.answer(approved=False) for item in pending)
508
+ try:
509
+ async with aclosing(self._round(
510
+ self._translator.answer_message(answers), stream=False, previous_usage=None,
511
+ )) as execution:
512
+ async for item in execution:
513
+ if isinstance(item, _Round):
514
+ if not item.local_approval:
515
+ self._cleanup_usage.append(item.output.usage)
516
+ if item.output.termination.status is TerminationStatus.FAILED:
517
+ self._cleanup_failure = item.output.termination
518
+ self._broken = True
519
+ raise RuntimeError("Native agent execution failed")
520
+ return item.response
521
+ raise RuntimeError("native decline produced no response")
522
+ except Exception:
523
+ # ApprovalLoop's best-effort cleanup catches errors; retain failure so
524
+ # it cannot turn this native failure into an approval-budget result.
525
+ self._broken = True
526
+ self._cleanup_failure = self._cleanup_failure or Termination(
527
+ TerminationStatus.FAILED, error=ErrorEnvelope(
528
+ code="native_execution_failed", category="handler_execution", message="Native agent execution failed",
529
+ ),
530
+ )
531
+ raise
532
+
533
+ def discard_authorizations(self) -> None:
534
+ """Clear domain grants on abandonment, error or cancellation."""
535
+ if self._discard is not None:
536
+ self._discard()
537
+
538
+ def final_text(self, state: AgentResponse[Any]) -> str:
539
+ """Extract normal-mode framework text.
540
+
541
+ Args:
542
+ state: Completed native response.
543
+ """
544
+ return state.text
@@ -0,0 +1,75 @@
1
+ """Inspectable conversion results, without retaining sensitive native payloads."""
2
+
3
+ from dataclasses import dataclass
4
+ from enum import StrEnum
5
+
6
+ from ygo74.agent_runtime.domains.contracts.agent_output import AgentOutput
7
+ from ygo74.agent_runtime.domains.contracts.stream_events import AgentStreamEvent
8
+
9
+
10
+ class ConversionStatus(StrEnum):
11
+ """Names whether framework conversion succeeded, partially succeeded with unsupported content, or excluded the value.
12
+ """
13
+ CONVERTED = "converted"
14
+ EXCLUDED = "excluded"
15
+ UNSUPPORTED = "unsupported"
16
+
17
+
18
+ class ConversionReason(StrEnum):
19
+ """Stable diagnostic categories explaining why a native framework value was excluded or unsupported.
20
+ """
21
+ SUPPORTED = "supported"
22
+ REASONING_NOT_SELECTED = "reasoning_not_selected"
23
+ PROTECTED_REASONING = "protected_reasoning"
24
+ NON_OUTPUT_ROLE = "non_output_role"
25
+ UNKNOWN_CONTENT = "unknown_content"
26
+ INVALID_JSON = "invalid_json"
27
+ MISSING_TOOL_IDENTITY = "missing_tool_identity"
28
+ STRUCTURED_TOOL_RESULT = "structured_tool_result"
29
+ TOOL_ERROR = "tool_error"
30
+ UNSUPPORTED_MEDIA = "unsupported_media"
31
+ INCOMPLETE_USAGE = "incomplete_usage"
32
+ UNKNOWN_FINISH_REASON = "unknown_finish_reason"
33
+ STRUCTURED_RESPONSE_VALUE = "structured_response_value"
34
+ PROVIDER_ERROR = "provider_error"
35
+ UNSUPPORTED_ANNOTATIONS = "unsupported_annotations"
36
+
37
+
38
+ @dataclass(frozen=True, slots=True)
39
+ class ConversionDecision:
40
+ """Records one native content item’s conversion status, reason, and safe diagnostic metadata.
41
+
42
+ Args:
43
+ content_id (str): Stable identity correlating one content item across start, delta, and end events.
44
+ native_type (str): Framework-native content type being converted.
45
+ status (ConversionStatus): Success, incomplete, or failed outcome for the operation.
46
+ reason (ConversionReason): Reason code or message associated with this decision or failure.
47
+ """
48
+ content_id: str
49
+ native_type: str
50
+ status: ConversionStatus
51
+ reason: ConversionReason
52
+
53
+
54
+ @dataclass(frozen=True, slots=True)
55
+ class OutputConversion:
56
+ """Bundles the neutral final result with all conversion decisions made while reading an SDK response.
57
+
58
+ Args:
59
+ output (AgentOutput): Typed agent output being validated, filtered, or projected.
60
+ decisions (tuple[ConversionDecision, ...]): Per-content conversion or filtering decisions returned to the caller.
61
+ """
62
+ output: AgentOutput
63
+ decisions: tuple[ConversionDecision, ...]
64
+
65
+
66
+ @dataclass(frozen=True, slots=True)
67
+ class UpdateConversion:
68
+ """Bundles typed stream events with the conversion decisions for one SDK update.
69
+
70
+ Args:
71
+ events (tuple[AgentStreamEvent, ...]): The ordered typed events that describe the streamed response.
72
+ decisions (tuple[ConversionDecision, ...]): Per-content conversion or filtering decisions returned to the caller.
73
+ """
74
+ events: tuple[AgentStreamEvent, ...]
75
+ decisions: tuple[ConversionDecision, ...]