ariacompute-agent 0.17.0__tar.gz

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,95 @@
1
+ Metadata-Version: 2.4
2
+ Name: ariacompute-agent
3
+ Version: 0.17.0
4
+ Summary: Aria agent SDK - mirrors the OpenAI Agents SDK (Agent / Runner.run / Runner.run_streamed / function_tool / Session) on top of the aria-agent-cloud beta Agents API.
5
+ License: MIT
6
+ Keywords: agent,agents-sdk,aria,ariacompute,openai
7
+ Requires-Python: >=3.10
8
+ Description-Content-Type: text/markdown
9
+ Provides-Extra: dev
10
+ Requires-Dist: pytest; extra == "dev"
11
+
12
+ # `ariacompute-agent`
13
+
14
+ The Aria agent SDK for Python. The API mirrors the
15
+ [OpenAI Agents SDK](https://developers.openai.com/api/docs/guides/agents/quickstart)
16
+ (`Agent`, `Runner.run`, `Runner.run_streamed`, `function_tool`, `Session`) and
17
+ talks to an `aria-agent-cloud` deployment through its **beta Agents** REST API.
18
+
19
+ ```bash
20
+ pip install ariacompute-agent
21
+ ```
22
+
23
+ ```python
24
+ import asyncio
25
+
26
+ from ariacompute_agent import Agent, Runner
27
+
28
+
29
+ async def main() -> None:
30
+ agent = Agent(
31
+ name="History tutor",
32
+ instructions="Answer history questions clearly and concisely.",
33
+ model="gpt-4o-mini",
34
+ )
35
+ result = await Runner.run(agent, "When did the Roman Empire fall?")
36
+ print(result.final_output)
37
+
38
+
39
+ asyncio.run(main())
40
+ ```
41
+
42
+ ## Tools
43
+
44
+ ```python
45
+ from ariacompute_agent import function_tool
46
+
47
+
48
+ @function_tool
49
+ def history_fun_fact() -> str:
50
+ """Return a short history fact."""
51
+ return "Sharks are older than trees."
52
+
53
+
54
+ agent = Agent(name="History tutor", tools=[history_fun_fact])
55
+ ```
56
+
57
+ The cloud executes tools inside its own sandbox, so only the schema
58
+ (`name` / `description` / `parameters`) is sent to the server.
59
+
60
+ ## Streaming
61
+
62
+ ```python
63
+ streamed = await Runner.run_streamed(agent, "Tell me something surprising")
64
+ async for ev in streamed.events:
65
+ if ev["type"] == "agent.turn.output_text.delta":
66
+ print(ev["delta"], end="")
67
+ result = await streamed.completed
68
+ print(result.final_output)
69
+ ```
70
+
71
+ Frames use the `agent.*` envelope; `agent.turn.completed` (or
72
+ `agent.turn.failed`) is the **only** terminal event — there is no
73
+ `data: [DONE]` sentinel.
74
+
75
+ ## Sessions
76
+
77
+ ```python
78
+ session = Session.create(agent)
79
+ await Runner.run(agent, "hello", session=session)
80
+ await Runner.run(agent, "and then?", session=session)
81
+ ```
82
+
83
+ ## Configuration
84
+
85
+ | Option | Environment variable | Default |
86
+ | --- | --- | --- |
87
+ | `base_url` | `ARIA_AGENT_BASE_URL` | `http://localhost:3000` |
88
+ | `api_key` | `ARIA_AGENT_API_KEY` | _(none)_ |
89
+ | `beta_header` | – | `agents=v1` (`OpenAI-Beta`) |
90
+
91
+ ## Tests
92
+
93
+ ```bash
94
+ python3 -m unittest discover -s tests
95
+ ```
@@ -0,0 +1,84 @@
1
+ # `ariacompute-agent`
2
+
3
+ The Aria agent SDK for Python. The API mirrors the
4
+ [OpenAI Agents SDK](https://developers.openai.com/api/docs/guides/agents/quickstart)
5
+ (`Agent`, `Runner.run`, `Runner.run_streamed`, `function_tool`, `Session`) and
6
+ talks to an `aria-agent-cloud` deployment through its **beta Agents** REST API.
7
+
8
+ ```bash
9
+ pip install ariacompute-agent
10
+ ```
11
+
12
+ ```python
13
+ import asyncio
14
+
15
+ from ariacompute_agent import Agent, Runner
16
+
17
+
18
+ async def main() -> None:
19
+ agent = Agent(
20
+ name="History tutor",
21
+ instructions="Answer history questions clearly and concisely.",
22
+ model="gpt-4o-mini",
23
+ )
24
+ result = await Runner.run(agent, "When did the Roman Empire fall?")
25
+ print(result.final_output)
26
+
27
+
28
+ asyncio.run(main())
29
+ ```
30
+
31
+ ## Tools
32
+
33
+ ```python
34
+ from ariacompute_agent import function_tool
35
+
36
+
37
+ @function_tool
38
+ def history_fun_fact() -> str:
39
+ """Return a short history fact."""
40
+ return "Sharks are older than trees."
41
+
42
+
43
+ agent = Agent(name="History tutor", tools=[history_fun_fact])
44
+ ```
45
+
46
+ The cloud executes tools inside its own sandbox, so only the schema
47
+ (`name` / `description` / `parameters`) is sent to the server.
48
+
49
+ ## Streaming
50
+
51
+ ```python
52
+ streamed = await Runner.run_streamed(agent, "Tell me something surprising")
53
+ async for ev in streamed.events:
54
+ if ev["type"] == "agent.turn.output_text.delta":
55
+ print(ev["delta"], end="")
56
+ result = await streamed.completed
57
+ print(result.final_output)
58
+ ```
59
+
60
+ Frames use the `agent.*` envelope; `agent.turn.completed` (or
61
+ `agent.turn.failed`) is the **only** terminal event — there is no
62
+ `data: [DONE]` sentinel.
63
+
64
+ ## Sessions
65
+
66
+ ```python
67
+ session = Session.create(agent)
68
+ await Runner.run(agent, "hello", session=session)
69
+ await Runner.run(agent, "and then?", session=session)
70
+ ```
71
+
72
+ ## Configuration
73
+
74
+ | Option | Environment variable | Default |
75
+ | --- | --- | --- |
76
+ | `base_url` | `ARIA_AGENT_BASE_URL` | `http://localhost:3000` |
77
+ | `api_key` | `ARIA_AGENT_API_KEY` | _(none)_ |
78
+ | `beta_header` | – | `agents=v1` (`OpenAI-Beta`) |
79
+
80
+ ## Tests
81
+
82
+ ```bash
83
+ python3 -m unittest discover -s tests
84
+ ```
@@ -0,0 +1,75 @@
1
+ """``ariacompute-agent`` — the Aria agent SDK.
2
+
3
+ The API mirrors the OpenAI Agents SDK (``openai-agents``) so the official
4
+ quickstart transfers unchanged:
5
+
6
+ ```python
7
+ import asyncio
8
+
9
+ from ariacompute_agent import Agent, Runner
10
+
11
+
12
+ async def main() -> None:
13
+ agent = Agent(
14
+ name="History tutor",
15
+ instructions="Answer history questions clearly and concisely.",
16
+ model="gpt-4o-mini",
17
+ )
18
+ result = await Runner.run(agent, "When did the Roman Empire fall?")
19
+ print(result.final_output)
20
+
21
+
22
+ asyncio.run(main())
23
+ ```
24
+ """
25
+
26
+ from .agent import Agent
27
+ from .memory import (
28
+ CloudMemoryStore,
29
+ CompositeMemoryStore,
30
+ LocalMemoryStore,
31
+ MemoryBackend,
32
+ )
33
+ from .runner import Runner
34
+ from .session import Session
35
+ from .tool import function_tool, tool_from_function
36
+ from .transport import (
37
+ DEFAULT_BETA_HEADER,
38
+ AgentTransportError,
39
+ get_json,
40
+ post_json,
41
+ resolve_client,
42
+ )
43
+ from .types import (
44
+ ClientOptions,
45
+ HistoryInput,
46
+ HistoryItem,
47
+ RunResult,
48
+ StreamedRunResult,
49
+ ToolSpec,
50
+ )
51
+
52
+ __all__ = [
53
+ "Agent",
54
+ "Runner",
55
+ "Session",
56
+ "MemoryBackend",
57
+ "LocalMemoryStore",
58
+ "CloudMemoryStore",
59
+ "CompositeMemoryStore",
60
+ "function_tool",
61
+ "tool_from_function",
62
+ "ClientOptions",
63
+ "HistoryItem",
64
+ "HistoryInput",
65
+ "RunResult",
66
+ "StreamedRunResult",
67
+ "ToolSpec",
68
+ "AgentTransportError",
69
+ "resolve_client",
70
+ "post_json",
71
+ "get_json",
72
+ "DEFAULT_BETA_HEADER",
73
+ ]
74
+
75
+ __version__ = "0.1.0"
@@ -0,0 +1,83 @@
1
+ """``Agent`` — mirrors ``Agent(...)`` from the OpenAI Agents SDK."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, Optional, Sequence, Union
6
+
7
+ from .memory import MemoryBackend
8
+ from .tool import tool_from_function
9
+ from .types import ClientOptions, ToolSpec
10
+
11
+
12
+ class Agent:
13
+ """A reusable agent definition.
14
+
15
+ ```python
16
+ agent = Agent(
17
+ name="History tutor",
18
+ instructions="Answer history questions clearly and concisely.",
19
+ model="gpt-4o-mini",
20
+ )
21
+ ```
22
+ """
23
+
24
+ def __init__(
25
+ self,
26
+ name: str,
27
+ instructions: Optional[str] = None,
28
+ model: Optional[str] = None,
29
+ tools: Optional[Sequence[Any]] = None,
30
+ handoffs: Optional[Sequence["Agent"]] = None,
31
+ handoff_description: Optional[str] = None,
32
+ id: Optional[str] = None,
33
+ client: Optional[ClientOptions] = None,
34
+ memory_backend: Union[str, MemoryBackend, None] = None,
35
+ memo_db: Optional[str] = None,
36
+ ) -> None:
37
+ if not name or not name.strip():
38
+ raise ValueError("Agent requires a `name`")
39
+ self.name = name
40
+ self.instructions = instructions
41
+ self.model = model
42
+ self.tools = [self._coerce_tool(t) for t in (tools or [])]
43
+ self.handoffs = list(handoffs or [])
44
+ self.handoff_description = handoff_description
45
+ self.id = id
46
+ self.client = client or ClientOptions()
47
+ self.memory_backend = (
48
+ MemoryBackend.parse(memory_backend) if memory_backend is not None
49
+ else MemoryBackend.CLOUD
50
+ )
51
+ self.memo_db = memo_db
52
+
53
+ @staticmethod
54
+ def _coerce_tool(t: Any) -> ToolSpec:
55
+ if isinstance(t, ToolSpec):
56
+ return t
57
+ if callable(t):
58
+ return tool_from_function(t)
59
+ raise TypeError(f"unsupported tool: {t!r}")
60
+
61
+ def clone(self, **overrides: Any) -> "Agent":
62
+ """Return a copy with overridden fields (used by handoffs)."""
63
+ params: dict[str, Any] = dict(
64
+ name=self.name,
65
+ instructions=self.instructions,
66
+ model=self.model,
67
+ tools=self.tools,
68
+ handoffs=self.handoffs,
69
+ handoff_description=self.handoff_description,
70
+ id=self.id,
71
+ client=self.client,
72
+ memory_backend=self.memory_backend,
73
+ memo_db=self.memo_db,
74
+ )
75
+ params.update(overrides)
76
+ return Agent(**params)
77
+
78
+ def tool_schemas(self) -> list[dict[str, Any]]:
79
+ """JSON-schema tool declarations forwarded to the cloud."""
80
+ return [t.schema() for t in self.tools]
81
+
82
+ def __repr__(self) -> str: # pragma: no cover - debugging aid
83
+ return f"Agent(name={self.name!r}, model={self.model!r})"
@@ -0,0 +1,216 @@
1
+ """Memory backends for :mod:`ariacompute_agent`.
2
+
3
+ * ``cloud`` — the agent-cloud session memory REST endpoints
4
+ (``/v1/agents/sessions/{id}/memory``).
5
+ * ``local`` — **aria memo**: the same SQLite ``memories`` database the
6
+ ``aria-memo`` product/CLI uses, written directly through the stdlib
7
+ :mod:`sqlite3` module (no external binary required). ``aria-memo list --json``
8
+ can read the very same file.
9
+ * ``both`` — writes to local **and** cloud; reads merged (cloud wins, local is
10
+ the fallback).
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import json
16
+ import sqlite3
17
+ from enum import Enum
18
+ from pathlib import Path
19
+ from typing import Optional, Protocol
20
+
21
+ from .transport import get_json, post_json, resolve_client
22
+ from .types import ClientOptions
23
+
24
+
25
+ class MemoryBackend(str, Enum):
26
+ """Where a memory context lives."""
27
+
28
+ CLOUD = "cloud"
29
+ LOCAL = "local"
30
+ BOTH = "both"
31
+
32
+ @classmethod
33
+ def parse(cls, value: Optional[object]) -> "MemoryBackend":
34
+ if value is None:
35
+ return cls.CLOUD
36
+ if isinstance(value, cls):
37
+ return value
38
+ raw = str(getattr(value, "value", value)).strip().lower()
39
+ for member in cls:
40
+ if member.value == raw:
41
+ return member
42
+ raise ValueError(f"unknown memory backend: {value}")
43
+
44
+
45
+ class MemoryStore(Protocol):
46
+ """Key/value view of a memory backend."""
47
+
48
+ backend: MemoryBackend
49
+
50
+ def put(self, key: str, value: str) -> None: # pragma: no cover - protocol
51
+ ...
52
+
53
+ def get(self, key: str) -> Optional[str]: # pragma: no cover - protocol
54
+ ...
55
+
56
+
57
+ # --- aria memo (local) -------------------------------------------------------
58
+
59
+ #: aria memo's ``memories`` schema — kept byte-identical so its CLI can open the file.
60
+ ARIA_MEMO_SCHEMA = """
61
+ CREATE TABLE IF NOT EXISTS memories (
62
+ id TEXT PRIMARY KEY,
63
+ memo_type TEXT NOT NULL,
64
+ content TEXT NOT NULL,
65
+ embedding BLOB,
66
+ metadata TEXT NOT NULL,
67
+ importance REAL NOT NULL,
68
+ version INTEGER NOT NULL,
69
+ created_at INTEGER NOT NULL,
70
+ updated_at INTEGER NOT NULL,
71
+ deleted INTEGER NOT NULL DEFAULT 0
72
+ );
73
+ CREATE INDEX IF NOT EXISTS idx_memories_type ON memories(memo_type);
74
+ CREATE INDEX IF NOT EXISTS idx_memories_updated_at ON memories(updated_at);
75
+ CREATE INDEX IF NOT EXISTS idx_memories_deleted ON memories(deleted);
76
+ """
77
+
78
+ MEMO_TYPE_LONG_TERM = "long_term:semantic"
79
+
80
+
81
+ def _now_sec() -> int:
82
+ import time
83
+
84
+ return int(time.time())
85
+
86
+
87
+ class LocalMemoryStore:
88
+ """``local`` backend: aria memo (SQLite, same format as ``aria-memo``)."""
89
+
90
+ backend = MemoryBackend.LOCAL
91
+
92
+ def __init__(self, db_path: Optional[str] = None) -> None:
93
+ path = db_path or __import__("os").environ.get("ARIA_MEMO_DB") or "memo.db"
94
+ self.db_path = str(path)
95
+ parent = Path(self.db_path).parent
96
+ if str(parent) and not parent.exists():
97
+ parent.mkdir(parents=True, exist_ok=True)
98
+ self._ensure_schema()
99
+
100
+ def _connect(self) -> sqlite3.Connection:
101
+ conn = sqlite3.connect(self.db_path)
102
+ conn.execute("PRAGMA journal_mode=WAL")
103
+ return conn
104
+
105
+ def _ensure_schema(self) -> None:
106
+ with self._connect() as conn:
107
+ conn.executescript(ARIA_MEMO_SCHEMA)
108
+
109
+ def put(self, key: str, value: str) -> None:
110
+ """Write ``key`` as a long-term aria memo (``metadata.key`` holds the key)."""
111
+ now = _now_sec()
112
+ metadata = json.dumps({"key": key})
113
+ with self._connect() as conn:
114
+ conn.execute(
115
+ "INSERT INTO memories "
116
+ "(id, memo_type, content, embedding, metadata, importance, version, "
117
+ " created_at, updated_at, deleted) "
118
+ "VALUES (?, ?, ?, ?, ?, ?, 1, ?, ?, 0) "
119
+ "ON CONFLICT(id) DO UPDATE SET "
120
+ " content = excluded.content, metadata = excluded.metadata, "
121
+ " updated_at = excluded.updated_at, deleted = 0",
122
+ (f"key:{key}", MEMO_TYPE_LONG_TERM, value, b"", metadata, 0.8, now, now),
123
+ )
124
+
125
+ def get(self, key: str) -> Optional[str]:
126
+ with self._connect() as conn:
127
+ rows = conn.execute(
128
+ "SELECT content, metadata FROM memories WHERE deleted = 0",
129
+ ).fetchall()
130
+ for content, metadata in rows:
131
+ try:
132
+ parsed = json.loads(metadata or "{}")
133
+ except json.JSONDecodeError:
134
+ parsed = {}
135
+ if parsed.get("key") == key:
136
+ return content
137
+ return None
138
+
139
+
140
+ # --- cloud -------------------------------------------------------------------
141
+
142
+
143
+ class CloudMemoryStore:
144
+ """``cloud`` backend: the agent-cloud session memory REST endpoints."""
145
+
146
+ backend = MemoryBackend.CLOUD
147
+
148
+ def __init__(self, session_id: str, client: Optional[ClientOptions] = None) -> None:
149
+ self.session_id = session_id
150
+ self.client = client or ClientOptions()
151
+
152
+ def _base(self) -> str:
153
+ return f"/v1/agents/sessions/{self.session_id}/memory"
154
+
155
+ def put(self, key: str, value: str) -> None:
156
+ resolved = resolve_client(self.client)
157
+ post_json(resolved, self._base(), {"key": key, "value": value, "kind": "long_term"})
158
+
159
+ def get(self, key: str) -> Optional[str]:
160
+ resolved = resolve_client(self.client)
161
+ res = get_json(resolved, f"{self._base()}/{key}")
162
+ return res.get("value")
163
+
164
+
165
+ # --- both --------------------------------------------------------------------
166
+
167
+
168
+ class CompositeMemoryStore:
169
+ """``both`` backend: writes to local **and** cloud, reads merged."""
170
+
171
+ backend = MemoryBackend.BOTH
172
+
173
+ def __init__(self, local: MemoryStore, cloud: MemoryStore) -> None:
174
+ self.local = local
175
+ self.cloud = cloud
176
+
177
+ def put(self, key: str, value: str) -> None:
178
+ errors = []
179
+ for store in (self.local, self.cloud):
180
+ try:
181
+ store.put(key, value)
182
+ except Exception as e: # noqa: BLE001 - one side may be offline
183
+ errors.append(str(e))
184
+ if len(errors) == 2:
185
+ raise RuntimeError("memory write failed on every backend: " + "; ".join(errors))
186
+
187
+ def get(self, key: str) -> Optional[str]:
188
+ errors = []
189
+ for store in (self.cloud, self.local):
190
+ try:
191
+ value = store.get(key)
192
+ except Exception as e: # noqa: BLE001 - fall through to the other side
193
+ errors.append(str(e))
194
+ continue
195
+ if value is not None:
196
+ return value
197
+ if len(errors) == 2:
198
+ raise RuntimeError("memory read failed on every backend: " + "; ".join(errors))
199
+ return None
200
+
201
+
202
+ def build_store(
203
+ backend: MemoryBackend,
204
+ session_id: str,
205
+ client: Optional[ClientOptions] = None,
206
+ memo_db: Optional[str] = None,
207
+ ) -> MemoryStore:
208
+ """Build the store for ``backend`` (local is created lazily but validated)."""
209
+ if backend is MemoryBackend.LOCAL:
210
+ return LocalMemoryStore(memo_db)
211
+ if backend is MemoryBackend.CLOUD:
212
+ return CloudMemoryStore(session_id, client)
213
+ return CompositeMemoryStore(
214
+ LocalMemoryStore(memo_db),
215
+ CloudMemoryStore(session_id, client),
216
+ )
@@ -0,0 +1,153 @@
1
+ """``Runner`` — mirrors ``Runner.run`` / ``Runner.run_streamed`` from the Agents SDK.
2
+
3
+ Both drive one turn against the cloud's beta Agents API:
4
+ ``POST /v1/agents/sessions/{id}/events`` (blocking) or
5
+ ``POST /v1/agents/sessions/{id}/events/stream`` (SSE).
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import asyncio
11
+ from typing import Any, AsyncIterator, Optional, Union
12
+
13
+ from .session import Session
14
+ from .transport import async_stream_json, post_json, resolve_client
15
+ from .types import ClientOptions, HistoryInput, HistoryItem, RunResult, StreamedRunResult
16
+
17
+
18
+ def _to_text(value: HistoryInput) -> str:
19
+ if isinstance(value, str):
20
+ return value
21
+ if isinstance(value, HistoryItem):
22
+ return value.content
23
+ if isinstance(value, dict): # {"role": ..., "content": ...}
24
+ return str(value.get("content", ""))
25
+ parts: list[str] = []
26
+ for item in value or []:
27
+ if isinstance(item, str):
28
+ parts.append(item)
29
+ elif isinstance(item, HistoryItem):
30
+ parts.append(item.content)
31
+ elif isinstance(item, dict):
32
+ parts.append(str(item.get("content", "")))
33
+ return "\n".join(parts)
34
+
35
+
36
+ def _ensure_session(agent: Any, client: ClientOptions, session: Any) -> Session:
37
+ if isinstance(session, Session):
38
+ return session
39
+ if isinstance(session, str) and session:
40
+ return Session(session, client)
41
+ return Session.create(agent, client)
42
+
43
+
44
+ class Runner:
45
+ """Runs agents against the aria-agent-cloud service."""
46
+
47
+ @staticmethod
48
+ async def run(
49
+ agent: Any,
50
+ input: HistoryInput,
51
+ *,
52
+ session: Optional[Union[Session, str]] = None,
53
+ client: Optional[ClientOptions] = None,
54
+ ) -> RunResult:
55
+ """Run one turn to completion and return the result.
56
+
57
+ ```python
58
+ result = await Runner.run(agent, "When did the Roman Empire fall?")
59
+ print(result.final_output)
60
+ ```
61
+ """
62
+ merged = client or agent.client
63
+ resolved = resolve_client(merged)
64
+ sess = _ensure_session(agent, merged, session)
65
+ text = _to_text(input)
66
+
67
+ turn = await asyncio.to_thread(
68
+ post_json,
69
+ resolved,
70
+ f"/v1/agents/sessions/{sess.id}/events",
71
+ {"input": text},
72
+ )
73
+ output = turn.get("output") or ""
74
+ sess.record(input, output)
75
+ return RunResult(
76
+ final_output=output,
77
+ history=list(sess.history),
78
+ last_agent=agent,
79
+ session_id=sess.id,
80
+ turn_id=turn.get("id"),
81
+ )
82
+
83
+ @staticmethod
84
+ async def run_streamed(
85
+ agent: Any,
86
+ input: HistoryInput,
87
+ *,
88
+ session: Optional[Union[Session, str]] = None,
89
+ client: Optional[ClientOptions] = None,
90
+ ) -> StreamedRunResult:
91
+ """Run one turn, streaming events (``agent.turn.*`` frames).
92
+
93
+ ```python
94
+ streamed = await Runner.run_streamed(agent, "Tell me something surprising")
95
+ async for ev in streamed.events:
96
+ if ev["type"] == "agent.turn.output_text.delta":
97
+ print(ev["delta"], end="")
98
+ result = await streamed.completed
99
+ print(result.final_output)
100
+ ```
101
+ """
102
+ merged = client or agent.client
103
+ resolved = resolve_client(merged)
104
+ sess = _ensure_session(agent, merged, session)
105
+ text = _to_text(input)
106
+
107
+ state: dict[str, Any] = {"text": "", "turn_id": None, "error": None}
108
+ buffered: list[dict[str, Any]] = []
109
+ drained = asyncio.Event()
110
+
111
+ def _note(frame: dict[str, Any]) -> None:
112
+ state["turn_id"] = frame.get("turn_id") or state["turn_id"]
113
+ kind = frame.get("type")
114
+ if kind == "agent.turn.output_text.delta":
115
+ state["text"] += str(frame.get("delta") or "")
116
+ elif kind in ("agent.turn.output_text.done", "agent.turn.completed"):
117
+ out = frame.get("text") or (frame.get("turn") or {}).get("output")
118
+ if isinstance(out, str) and out:
119
+ state["text"] = out
120
+ elif kind == "agent.turn.failed":
121
+ state["error"] = (frame.get("error") or {}).get("message") or "run failed"
122
+
123
+ async def _drain() -> None:
124
+ async for frame in async_stream_json(
125
+ resolved,
126
+ f"/v1/agents/sessions/{sess.id}/events/stream",
127
+ {"input": text},
128
+ ):
129
+ buffered.append(frame)
130
+ _note(frame)
131
+ drained.set()
132
+
133
+ drain_task = asyncio.ensure_future(_drain())
134
+
135
+ async def events() -> AsyncIterator[dict[str, Any]]:
136
+ await asyncio.shield(drain_task)
137
+ for frame in buffered:
138
+ yield frame
139
+
140
+ async def completed() -> RunResult:
141
+ await asyncio.shield(drain_task)
142
+ if state["error"]:
143
+ raise RuntimeError(state["error"])
144
+ sess.record(input, state["text"])
145
+ return RunResult(
146
+ final_output=state["text"],
147
+ history=list(sess.history),
148
+ last_agent=agent,
149
+ session_id=sess.id,
150
+ turn_id=state["turn_id"],
151
+ )
152
+
153
+ return StreamedRunResult(events=events(), completed=completed())