beadrun 0.1.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.
beadrun/__init__.py ADDED
@@ -0,0 +1,16 @@
1
+ """bead sandboxes: one key, natural objects, automatic resource cleanup."""
2
+ from .account import Bead as Bead, limits as limits, pricing as pricing
3
+ from .async_account import AsyncBead as AsyncBead
4
+ from .sandbox import Sandbox as Sandbox
5
+ from .async_sandbox import AsyncSandbox as AsyncSandbox
6
+ from .batches import Fleet as Fleet, Swarm as Swarm
7
+ from .async_batches import AsyncFleet as AsyncFleet, AsyncSwarm as AsyncSwarm
8
+ from .resources import Golden as Golden
9
+ from .async_resources import AsyncGolden as AsyncGolden
10
+ from .models import CommandResult as CommandResult, CommandEvent as CommandEvent, FileEntry as FileEntry
11
+ from .errors import (
12
+ BeadError as BeadError, AuthenticationError as AuthenticationError, NotFoundError as NotFoundError,
13
+ TimeoutError as TimeoutError, ProtocolError as ProtocolError, LimitError as LimitError,
14
+ RateLimitError as RateLimitError, PaymentRequiredError as PaymentRequiredError, BatchError as BatchError,
15
+ AdmissionLimit as AdmissionLimit,
16
+ )
beadrun/account.py ADDED
@@ -0,0 +1,46 @@
1
+ """Account-level usage, pricing and credential profiles."""
2
+ from __future__ import annotations
3
+ from typing import Any, Unpack
4
+ from .models import ClientOptions
5
+ from .resources import Profiles
6
+ from .transport import Transport
7
+
8
+
9
+ class Pricing:
10
+ def __init__(self, transport: Transport | None = None):
11
+ self.transport = transport
12
+
13
+ def estimate(self, *, vcpus: int, memory_mib: int, disk_mib: int, duration_seconds: int,
14
+ paused: bool = False, machines: int = 1, **client: Unpack[ClientOptions]) -> dict[str, Any]:
15
+ transport = self.transport or Transport(client)
16
+ try:
17
+ return dict(transport.json("POST", "/v1/pricing/estimate", {
18
+ "vcpus": vcpus, "memory_mib": memory_mib, "disk_mib": disk_mib,
19
+ "duration_seconds": duration_seconds, "paused": paused, "machines": machines,
20
+ }))
21
+ finally:
22
+ if self.transport is None:
23
+ transport.close()
24
+
25
+
26
+ class Bead:
27
+ def __init__(self, **client: Unpack[ClientOptions]):
28
+ self.transport = Transport(client)
29
+ self.profiles, self.pricing = Profiles(self.transport), Pricing(self.transport)
30
+
31
+ def limits(self) -> dict[str, Any]:
32
+ return dict(self.transport.json("GET", "/v1/limits"))
33
+
34
+ def close(self) -> None:
35
+ self.transport.close()
36
+
37
+
38
+ pricing = Pricing()
39
+
40
+
41
+ def limits(**client: Unpack[ClientOptions]) -> dict[str, Any]:
42
+ account = Bead(**client)
43
+ try:
44
+ return account.limits()
45
+ finally:
46
+ account.close()
@@ -0,0 +1,40 @@
1
+ # Generated by sdk/tools/generate_async.py; edit the sync owner.
2
+ from __future__ import annotations
3
+ 'Account-level usage, pricing and credential profiles.'
4
+ from typing import Any, Unpack
5
+ from .models import ClientOptions
6
+ from .async_resources import AsyncProfiles
7
+ from .async_transport import AsyncTransport
8
+
9
+ class AsyncPricing:
10
+
11
+ def __init__(self, transport: AsyncTransport | None=None):
12
+ self.transport = transport
13
+
14
+ async def estimate(self, *, vcpus: int, memory_mib: int, disk_mib: int, duration_seconds: int, paused: bool=False, machines: int=1, **client: Unpack[ClientOptions]) -> dict[str, Any]:
15
+ transport = self.transport or AsyncTransport(client)
16
+ try:
17
+ return dict(await transport.json('POST', '/v1/pricing/estimate', {'vcpus': vcpus, 'memory_mib': memory_mib, 'disk_mib': disk_mib, 'duration_seconds': duration_seconds, 'paused': paused, 'machines': machines}))
18
+ finally:
19
+ if self.transport is None:
20
+ await transport.close()
21
+
22
+ class AsyncBead:
23
+
24
+ def __init__(self, **client: Unpack[ClientOptions]):
25
+ self.transport = AsyncTransport(client)
26
+ self.profiles, self.pricing = (AsyncProfiles(self.transport), AsyncPricing(self.transport))
27
+
28
+ async def limits(self) -> dict[str, Any]:
29
+ return dict(await self.transport.json('GET', '/v1/limits'))
30
+
31
+ async def close(self) -> None:
32
+ await self.transport.close()
33
+ pricing = AsyncPricing()
34
+
35
+ async def limits(**client: Unpack[ClientOptions]) -> dict[str, Any]:
36
+ account = AsyncBead(**client)
37
+ try:
38
+ return await account.limits()
39
+ finally:
40
+ await account.close()
@@ -0,0 +1,80 @@
1
+ # Generated by sdk/tools/generate_async.py; edit the sync owner.
2
+ from __future__ import annotations
3
+ import asyncio
4
+ from collections.abc import Iterator
5
+ 'Server-side fleets and durable swarm result iteration.'
6
+ from collections.abc import AsyncIterator
7
+ from types import TracebackType
8
+ from typing import Any, Unpack
9
+ from urllib.parse import quote
10
+ import json
11
+ import time
12
+ from .errors import TimeoutError
13
+ from .models import ClientOptions
14
+ from .async_resources import async_wait_for
15
+ from .async_sandbox import AsyncSandbox, async_create_request
16
+ from .async_transport import AsyncTransport
17
+
18
+ class AsyncFleet:
19
+
20
+ def __init__(self, sandboxes: list[AsyncSandbox], transport: AsyncTransport):
21
+ self.sandboxes, self.transport = (sandboxes, transport)
22
+
23
+ @classmethod
24
+ async def create(cls, n: int, *, template: str='base', timeout: int=300, env: dict[str, str] | None=None, **client: Unpack[ClientOptions]) -> AsyncFleet:
25
+ transport = AsyncTransport(client)
26
+ batch = await transport.json('POST', '/v2/sandboxes/fleet', {'count': n, 'request': await async_create_request(transport, template, timeout, env, None, None, None)}, True)
27
+ if batch.get('fleetID'):
28
+ id = str(batch['fleetID'])
29
+ batch = await async_wait_for(transport, lambda: transport.json('GET', '/v2/sandboxes/fleets/' + quote(id, safe='')), lambda b: bool(b.get('results')) and all((r['status'] != 202 for r in b['results'])))
30
+ return cls(AsyncSandbox.members(batch.get('results', []), transport), transport)
31
+
32
+ def __iter__(self) -> Iterator[AsyncSandbox]:
33
+ return iter(self.sandboxes)
34
+
35
+ async def kill(self) -> None:
36
+ failures: list[Exception] = []
37
+ for sandbox in self.sandboxes:
38
+ try:
39
+ await sandbox.kill()
40
+ except Exception as exc:
41
+ failures.append(exc)
42
+ if failures:
43
+ raise failures[0]
44
+
45
+ async def __aenter__(self) -> AsyncFleet:
46
+ return self
47
+
48
+ async def __aexit__(self, exc_type: type[BaseException] | None, exc: BaseException | None, tb: TracebackType | None) -> None:
49
+ await self.kill()
50
+
51
+ class AsyncSwarm:
52
+
53
+ def __init__(self, id: str, transport: AsyncTransport):
54
+ self.id, self.transport = (id, transport)
55
+
56
+ @classmethod
57
+ async def run(cls, *, cmd: str, inputs: list[Any], template: str='base', timeout: int=300, concurrency: int=8, task_retries: int=0, output_files: list[str] | None=None, **client: Unpack[ClientOptions]) -> AsyncSwarm:
58
+ transport = AsyncTransport(client)
59
+ result = await transport.json('POST', '/swarms', {'template': template, 'command': cmd, 'inputs': inputs, 'time_limit_seconds': timeout, 'concurrency': concurrency, 'retries': task_retries, 'output_files': output_files or []}, True)
60
+ return cls(str(result['id']), transport)
61
+
62
+ async def status(self) -> dict[str, Any]:
63
+ return dict(await self.transport.json('GET', '/swarms/' + quote(self.id, safe='')))
64
+
65
+ async def cancel(self) -> None:
66
+ await self.transport.json('POST', '/swarms/' + quote(self.id, safe='') + '/cancel')
67
+
68
+ async def __aiter__(self) -> AsyncIterator[dict[str, Any]]:
69
+ deadline = time.monotonic() + self.transport.options.get('operation_timeout', 600)
70
+ while (await self.status())['state'] in ('queued', 'running'):
71
+ if time.monotonic() >= deadline:
72
+ raise TimeoutError('Swarm wait expired. Inspect status or call swarm.cancel().')
73
+ await asyncio.sleep(self.transport.options.get('poll_interval', 1))
74
+ async with self.transport.stream('GET', '/swarms/' + quote(self.id, safe='') + '/results') as response:
75
+ async for line in response.aiter_lines():
76
+ if line.strip():
77
+ yield json.loads(line)
78
+
79
+ async def close(self) -> None:
80
+ await self.transport.close()
@@ -0,0 +1,113 @@
1
+ # Generated by sdk/tools/generate_async.py; edit the sync owner.
2
+ from __future__ import annotations
3
+ from collections.abc import Awaitable
4
+ 'Connect streaming without a protobuf runtime dependency.'
5
+ from collections.abc import AsyncIterator
6
+ from typing import Any, Literal, Protocol, overload
7
+ import base64
8
+ import codecs
9
+ import json
10
+ import struct
11
+ import time
12
+ from .http_types import AsyncResponseProtocol
13
+ from .errors import ProtocolError, TimeoutError, api_error
14
+ from .models import CommandEvent, CommandResult, OutputCallback
15
+ from .async_transport import AsyncTransport
16
+
17
+ class AsyncGuest(Protocol):
18
+ transport: AsyncTransport
19
+
20
+ def guest(self) -> tuple[str, dict[str, str]]:
21
+ ...
22
+
23
+ async def async_frames(response: AsyncResponseProtocol) -> AsyncIterator[dict[str, Any]]:
24
+ buffer = b''
25
+ async for chunk in response.aiter_bytes():
26
+ buffer += chunk
27
+ while len(buffer) >= 5:
28
+ flags, length = struct.unpack('!BI', buffer[:5])
29
+ if length > 8 * 1024 * 1024:
30
+ raise ProtocolError('Command frame exceeds 8 MiB.')
31
+ if len(buffer) < 5 + length:
32
+ break
33
+ frame: dict[str, Any] = json.loads(buffer[5:5 + length])
34
+ buffer = buffer[5 + length:]
35
+ if flags == 2:
36
+ if frame.get('error'):
37
+ raise api_error(500, frame['error'])
38
+ if buffer:
39
+ raise ProtocolError('Unexpected bytes after stream trailer.')
40
+ return
41
+ if flags != 0:
42
+ raise ProtocolError('Unsupported Connect stream compression.')
43
+ yield frame
44
+ raise ProtocolError('Command stream ended without a Connect trailer; command may still be running.')
45
+
46
+ class AsyncCommands:
47
+
48
+ def __init__(self, sandbox: AsyncGuest):
49
+ self.sandbox = sandbox
50
+
51
+ @overload
52
+ def run(self, cmd: str, *, stream: Literal[True], cwd: str | None=None, env: dict[str, str] | None=None, timeout: float=60, on_stdout: OutputCallback | None=None, on_stderr: OutputCallback | None=None) -> AsyncIterator[CommandEvent]:
53
+ ...
54
+
55
+ @overload
56
+ def run(self, cmd: str, *, stream: Literal[False]=False, cwd: str | None=None, env: dict[str, str] | None=None, timeout: float=60, on_stdout: OutputCallback | None=None, on_stderr: OutputCallback | None=None) -> Awaitable[CommandResult]:
57
+ ...
58
+
59
+ def run(self, cmd: str, *, stream: bool=False, cwd: str | None=None, env: dict[str, str] | None=None, timeout: float=60, on_stdout: OutputCallback | None=None, on_stderr: OutputCallback | None=None) -> AsyncIterator[CommandEvent] | Awaitable[CommandResult]:
60
+ if stream:
61
+ return self.stream(cmd, cwd=cwd, env=env, timeout=timeout, on_stdout=on_stdout, on_stderr=on_stderr)
62
+ return self.collect(cmd, cwd=cwd, env=env, timeout=timeout, on_stdout=on_stdout, on_stderr=on_stderr)
63
+
64
+ async def collect(self, cmd: str, *, cwd: str | None=None, env: dict[str, str] | None=None, timeout: float=60, on_stdout: OutputCallback | None=None, on_stderr: OutputCallback | None=None) -> CommandResult:
65
+ output = {'stdout': '', 'stderr': ''}
66
+ exit_code = 0
67
+ async for event in self.stream(cmd, cwd=cwd, env=env, timeout=timeout, on_stdout=on_stdout, on_stderr=on_stderr):
68
+ if event.type in output:
69
+ output[event.type] += event.data
70
+ elif event.type == 'exit':
71
+ exit_code = event.exit_code or 0
72
+ return CommandResult(exit_code, output['stdout'], output['stderr'])
73
+
74
+ async def stream(self, cmd: str, *, cwd: str | None=None, env: dict[str, str] | None=None, timeout: float=60, on_stdout: OutputCallback | None=None, on_stderr: OutputCallback | None=None) -> AsyncIterator[CommandEvent]:
75
+ url, headers = self.sandbox.guest()
76
+ process: dict[str, Any] = {'cmd': '/bin/bash', 'args': ['-lc', cmd], 'envs': env or {}}
77
+ if cwd is not None:
78
+ process['cwd'] = cwd
79
+ headers.update({'content-type': 'application/connect+json', 'connect-protocol-version': '1', 'connect-timeout-ms': str(int(timeout * 1000))})
80
+ payload = json.dumps({'process': process, 'stdin': False}).encode()
81
+ envelope = struct.pack('!BI', 0, len(payload)) + payload
82
+ ended = False
83
+ decoders = {kind: codecs.getincrementaldecoder('utf-8')('replace') for kind in ('stdout', 'stderr')}
84
+ deadline = time.monotonic() + timeout
85
+ async with self.sandbox.transport.stream('POST', url + '/process.Process/Start', content=envelope, headers=headers, timeout=timeout) as response:
86
+ async for frame in async_frames(response):
87
+ if time.monotonic() > deadline:
88
+ raise TimeoutError('Command wait expired; kill the sandbox to stop all processes.')
89
+ event = frame.get('event', {})
90
+ if 'start' in event:
91
+ yield CommandEvent('start', pid=event['start']['pid'])
92
+ for kind in ('stdout', 'stderr'):
93
+ if kind in event.get('data', {}):
94
+ data = decoders[kind].decode(base64.b64decode(event['data'][kind]))
95
+ callback = on_stdout if kind == 'stdout' else on_stderr
96
+ if callback:
97
+ callback(data)
98
+ yield CommandEvent('stdout' if kind == 'stdout' else 'stderr', data=data)
99
+ if 'end' in event:
100
+ end = event['end']
101
+ if end.get('error') and (not end.get('exited')):
102
+ raise ProtocolError(str(end['error']))
103
+ ended = True
104
+ for kind in ('stdout', 'stderr'):
105
+ data = decoders[kind].decode(b'', final=True)
106
+ if data:
107
+ callback = on_stdout if kind == 'stdout' else on_stderr
108
+ if callback:
109
+ callback(data)
110
+ yield CommandEvent('stdout' if kind == 'stdout' else 'stderr', data=data)
111
+ yield CommandEvent('exit', exit_code=int(end.get('exitCode', 0)))
112
+ if not ended:
113
+ raise ProtocolError('Command stream closed without exit. Inspect command state before retrying.')
beadrun/async_files.py ADDED
@@ -0,0 +1,42 @@
1
+ # Generated by sdk/tools/generate_async.py; edit the sync owner.
2
+ from __future__ import annotations
3
+ import asyncio
4
+ 'Portable bytes and explicit local transfers.'
5
+ from pathlib import Path
6
+ from urllib.parse import quote
7
+ from typing import Any
8
+ import json
9
+ import builtins
10
+ from .async_commands import AsyncGuest
11
+ from .models import FileEntry
12
+
13
+ class AsyncFiles:
14
+
15
+ def __init__(self, sandbox: AsyncGuest):
16
+ self.sandbox = sandbox
17
+
18
+ async def read(self, path: str) -> str:
19
+ return (await self.read_bytes(path)).decode()
20
+
21
+ async def read_bytes(self, path: str) -> bytes:
22
+ url, headers = self.sandbox.guest()
23
+ return await self.sandbox.transport.request('GET', url + '/files?path=' + quote(path, safe=''), headers=headers)
24
+
25
+ async def write(self, path: str, content: str | bytes) -> None:
26
+ url, headers = self.sandbox.guest()
27
+ headers['content-type'] = 'application/octet-stream'
28
+ await self.sandbox.transport.request('POST', url + '/files?path=' + quote(path, safe=''), content.encode() if isinstance(content, str) else content, headers)
29
+
30
+ async def list(self, path: str='/') -> builtins.list[FileEntry]:
31
+ url, headers = self.sandbox.guest()
32
+ headers.update({'content-type': 'application/json', 'connect-protocol-version': '1'})
33
+ data: dict[str, Any] = json.loads(await self.sandbox.transport.request('POST', url + '/filesystem.Filesystem/ListDir', json.dumps({'path': path, 'depth': 1}).encode(), headers, True))
34
+ return [FileEntry(str(e['name']), str(e['path']), str(e.get('type', '')), int(e.get('size', 0)), str(e.get('permissions', ''))) for e in data.get('entries', [])]
35
+
36
+ async def upload(self, local_path: str | Path, remote_path: str) -> None:
37
+ await self.write(remote_path, await asyncio.to_thread(Path(local_path).read_bytes))
38
+
39
+ async def download(self, remote_path: str, local_path: str | Path) -> Path:
40
+ target = Path(local_path)
41
+ await asyncio.to_thread(target.write_bytes, await self.read_bytes(remote_path))
42
+ return target
beadrun/async_http.py ADDED
@@ -0,0 +1,91 @@
1
+ """Async HTTP using bounded urllib3 socket reads outside the event loop.
2
+
3
+ The sync HTTP owner supplies TLS, framing and timeout behavior. Cancellation
4
+ waits for the current bounded socket operation before releasing its connection.
5
+ """
6
+ from __future__ import annotations
7
+ from collections.abc import AsyncIterator, Callable, Mapping
8
+ from contextlib import asynccontextmanager
9
+ from typing import Any, TypeVar
10
+ import asyncio
11
+ import urllib3
12
+ from .http import HttpClient, Response
13
+ from .http_types import AsyncResponseProtocol, HTTPTimeoutError
14
+
15
+ T = TypeVar('T')
16
+
17
+
18
+ async def offload(operation: Callable[[], T]) -> T:
19
+ task = asyncio.create_task(asyncio.to_thread(operation))
20
+ try:
21
+ return await asyncio.shield(task)
22
+ except urllib3.exceptions.TimeoutError as exc:
23
+ raise HTTPTimeoutError() from exc
24
+ except asyncio.CancelledError:
25
+ try:
26
+ await task
27
+ finally:
28
+ raise
29
+
30
+
31
+ class AsyncResponse:
32
+ def __init__(self, raw: Response):
33
+ self.raw = raw
34
+ self.status_code = raw.status_code
35
+ self.headers: Mapping[str, str] = raw.headers
36
+
37
+ @property
38
+ def is_success(self) -> bool:
39
+ return self.raw.is_success
40
+
41
+ async def aread(self) -> bytes:
42
+ return await offload(self.raw.read)
43
+
44
+ def json(self) -> Any:
45
+ return self.raw.json()
46
+
47
+ async def aiter_bytes(self) -> AsyncIterator[bytes]:
48
+ iterator = self.raw.iter_bytes()
49
+ while True:
50
+ item = await offload(lambda: next(iterator, None))
51
+ if item is None:
52
+ return
53
+ yield item
54
+
55
+ async def aiter_lines(self) -> AsyncIterator[str]:
56
+ iterator = self.raw.iter_lines()
57
+ while True:
58
+ item = await offload(lambda: next(iterator, None))
59
+ if item is None:
60
+ return
61
+ yield item
62
+
63
+
64
+ class AsyncHttpClient:
65
+ def __init__(self, *, headers: dict[str, str]):
66
+ self.client = HttpClient(headers=headers)
67
+
68
+ @asynccontextmanager
69
+ async def stream(self, method: str, url: str, *, content: bytes | None,
70
+ headers: dict[str, str], timeout: float) -> AsyncIterator[AsyncResponseProtocol]:
71
+ context = self.client.stream(method, url, content=content, headers=headers, timeout=timeout)
72
+ task = asyncio.create_task(asyncio.to_thread(context.__enter__))
73
+ entered = False
74
+ try:
75
+ try:
76
+ raw = await asyncio.shield(task)
77
+ except asyncio.CancelledError:
78
+ await task
79
+ entered = True
80
+ raise
81
+ entered = True
82
+ # HttpClient is the sole concrete owner of this response.
83
+ if not isinstance(raw, Response):
84
+ raise TypeError('Unexpected HTTP response implementation')
85
+ yield AsyncResponse(raw)
86
+ finally:
87
+ if entered:
88
+ await offload(lambda: context.__exit__(None, None, None))
89
+
90
+ async def aclose(self) -> None:
91
+ await offload(self.client.close)
@@ -0,0 +1,100 @@
1
+ # Generated by sdk/tools/generate_async.py; edit the sync owner.
2
+ from __future__ import annotations
3
+ import asyncio
4
+ from collections.abc import Awaitable
5
+ 'Publication waits, cursor-based agent events and credential metadata.'
6
+ from collections.abc import Callable, AsyncIterator
7
+ from typing import Any, TypeVar
8
+ from urllib.parse import quote
9
+ import time
10
+ import builtins
11
+ import uuid
12
+ from .errors import BeadError, TimeoutError
13
+ from .async_transport import AsyncTransport
14
+ T = TypeVar('T')
15
+
16
+ async def async_wait_for(transport: AsyncTransport, read: Callable[[], Awaitable[T]], ready: Callable[[T], bool]) -> T:
17
+ deadline = time.monotonic() + transport.options.get('operation_timeout', 600)
18
+ while True:
19
+ value = await read()
20
+ if ready(value):
21
+ return value
22
+ if time.monotonic() >= deadline:
23
+ raise TimeoutError('Operation wait expired. Inspect resource status before retrying.')
24
+ await asyncio.sleep(transport.options.get('poll_interval', 1))
25
+
26
+ class AsyncGolden:
27
+
28
+ def __init__(self, info: dict[str, Any], transport: AsyncTransport):
29
+ self.info, self.transport = (info, transport)
30
+ self.deleted = False
31
+ transport.retain()
32
+ self.id = str(info['snapshotID'])
33
+
34
+ async def wait(self) -> AsyncGolden:
35
+
36
+ def ready(info: dict[str, Any]) -> bool:
37
+ if info['status'] in ('error', 'failed', 'retired', 'unavailable'):
38
+ raise BeadError('Golden publication failed. Inspect snapshot status and retry capture.')
39
+ return bool(info['status'] == 'ready')
40
+
41
+ async def read() -> dict[str, Any]:
42
+ return dict(await self.transport.json('GET', '/snapshots/' + quote(self.id, safe='')))
43
+ info: dict[str, Any] = await async_wait_for(self.transport, read, ready)
44
+ self.info.update(info)
45
+ return self
46
+
47
+ async def delete(self) -> None:
48
+ if not self.deleted:
49
+ await self.transport.json('DELETE', '/snapshots/' + quote(self.id, safe=''))
50
+ self.deleted = True
51
+ await self.transport.release()
52
+
53
+ class AsyncAgent:
54
+
55
+ def __init__(self, transport: AsyncTransport, path: str):
56
+ self.transport, self.path = (transport, path)
57
+
58
+ async def prompt(self, prompt: str, *, cwd: str='/home/user', profile_id: str | None=None, model: str | None=None, timeout: float=600, approval_policy: str='untrusted') -> AsyncIterator[dict[str, Any]]:
59
+ accepted = await self.transport.json('POST', self.path + '/prompt', {'requestId': str(uuid.uuid4()), 'prompt': prompt, 'cwd': cwd, 'profileId': profile_id, 'model': model, 'approvalPolicy': approval_policy})
60
+ cursor = max(0, int(accepted.get('cursor', 1)) - 1)
61
+ deadline = time.monotonic() + timeout
62
+ while True:
63
+ page = await self.transport.json('GET', self.path + f'/events?after={cursor}')
64
+ for event in page['events']:
65
+ turn = accepted['turnId']
66
+ if event.get('turnId') != turn:
67
+ continue
68
+ yield event
69
+ kind = event['event'].get('method', event['event'].get('type'))
70
+ if kind == 'bead/recoveryRequired':
71
+ raise BeadError('Agent recovery required. Inspect conversation status and restart the runner before prompting again.')
72
+ if turn and event.get('turnId') == turn and (kind in ('turn/completed', 'turn/failed', 'bead/terminal')):
73
+ return
74
+ cursor = int(page['cursor'])
75
+ if time.monotonic() >= deadline:
76
+ raise TimeoutError('Agent wait expired. Inspect conversation or interrupt the runner.')
77
+ await asyncio.sleep(self.transport.options.get('poll_interval', 1))
78
+
79
+ async def status(self) -> dict[str, Any]:
80
+ return dict(await self.transport.json('GET', self.path + '/conversation'))
81
+
82
+ async def interrupt(self, turn_id: str) -> Any:
83
+ return await self.transport.json('POST', self.path + '/runner/interrupt', {'requestId': str(uuid.uuid4()), 'expectedTurnId': turn_id})
84
+
85
+ class AsyncProfiles:
86
+
87
+ def __init__(self, transport: AsyncTransport):
88
+ self.transport = transport
89
+
90
+ async def list(self) -> builtins.list[dict[str, Any]]:
91
+ return list(await self.transport.json('GET', '/credential-profiles'))
92
+
93
+ async def put(self, id: str, name: str, material: dict[str, Any]) -> Any:
94
+ return await self.transport.json('PUT', '/credential-profiles/' + quote(id, safe=''), {'name': name, 'material': material})
95
+
96
+ async def revoke(self, id: str) -> None:
97
+ await self.transport.json('POST', '/credential-profiles/' + quote(id, safe='') + '/revoke')
98
+
99
+ async def delete(self, id: str) -> None:
100
+ await self.transport.json('DELETE', '/credential-profiles/' + quote(id, safe=''))