levain-runtime 0.1.2__tar.gz → 0.1.3__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.
@@ -18,6 +18,10 @@ build/
18
18
  !**/platform_agents/build/
19
19
  !**/src/build/
20
20
 
21
+ # Maven build output
22
+ services/flink/target/
23
+ services/flink/dependency-reduced-pom.xml
24
+
21
25
  # Binaries
22
26
  bin/
23
27
 
@@ -136,3 +140,6 @@ assets/video/*/renders/
136
140
  # www/public/videos/, the composition that made them does not.
137
141
  assets/video/updates-*/
138
142
  assets/video/*/snapshots/
143
+
144
+ # Maven build output (Flink job); the image build compiles from source.
145
+ services/flink/target/
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: levain-runtime
3
- Version: 0.1.2
3
+ Version: 0.1.3
4
4
  Summary: Self-hosted runtime for Levain agents: run an agent's nodes on your own machine or container while the platform drives the recipe
5
5
  License-Expression: Apache-2.0
6
6
  License-File: LICENSE
@@ -9,6 +9,7 @@ import os
9
9
  import shlex
10
10
  import subprocess
11
11
  import sys
12
+ import time
12
13
  from importlib import resources
13
14
  from pathlib import Path
14
15
  from typing import Annotated
@@ -283,6 +284,82 @@ def register(
283
284
  print("next: levain-runtime up")
284
285
 
285
286
 
287
+ def _detach_and_send(
288
+ root: Path,
289
+ creds: Credentials,
290
+ event_type: str,
291
+ payload: object,
292
+ agent: str | None,
293
+ ) -> None:
294
+ """Deliver an event from a detached child; the caller returns now.
295
+
296
+ A hook must never wait on the network, so the parent forks and
297
+ exits while the child drops the inherited stdio (a caller reading
298
+ our pipes would otherwise block on the child) and posts the event.
299
+ Failures land in ``emit.log`` under the runtime home.
300
+ """
301
+ if os.fork() > 0:
302
+ return
303
+ os.setsid()
304
+ devnull = os.open(os.devnull, os.O_RDWR)
305
+ for fd in (0, 1, 2):
306
+ os.dup2(devnull, fd)
307
+ try:
308
+ platform_api.post_event(creds, event_type, payload, agent)
309
+ except Exception as exc: # nothing above us to handle it
310
+ with (root / "emit.log").open("a") as fh:
311
+ fh.write(
312
+ f"{time.strftime('%Y-%m-%dT%H:%M:%S%z')} {event_type}: {exc}\n"
313
+ )
314
+ finally:
315
+ os._exit(0)
316
+
317
+
318
+ @app.command()
319
+ def emit(
320
+ event_type: Annotated[
321
+ str,
322
+ typer.Argument(help="Event name, e.g. runtime.claude_code.stop"),
323
+ ],
324
+ agent: Annotated[
325
+ str | None,
326
+ typer.Option(help="Only dispatch this agent (slug)"),
327
+ ] = None,
328
+ payload: Annotated[
329
+ str | None,
330
+ typer.Option(help="JSON payload, or - to read it from stdin"),
331
+ ] = None,
332
+ wait: Annotated[
333
+ bool,
334
+ typer.Option(
335
+ "--wait", help="Send in the foreground and print the result"
336
+ ),
337
+ ] = False,
338
+ ) -> None:
339
+ """Raise an event for agents subscribed to it on this runtime.
340
+
341
+ Reads the payload, then returns immediately while a detached child
342
+ delivers it — safe to call from hooks. Use --wait to send in the
343
+ foreground and see the platform's response.
344
+ """
345
+ config = _load_config()
346
+ creds = config.credentials
347
+ if creds is None:
348
+ raise typer.BadParameter(
349
+ "no credentials; run `levain-runtime connect`"
350
+ )
351
+ raw = sys.stdin.read() if payload == "-" else payload
352
+ try:
353
+ parsed = json.loads(raw) if raw and raw.strip() else None
354
+ except json.JSONDecodeError as exc:
355
+ raise typer.BadParameter(f"payload is not valid JSON: {exc}") from None
356
+ if wait or not hasattr(os, "fork"):
357
+ result = platform_api.post_event(creds, event_type, parsed, agent)
358
+ print(json.dumps(result))
359
+ return
360
+ _detach_and_send(config.root, creds, event_type, parsed, agent)
361
+
362
+
286
363
  @app.command()
287
364
  def up() -> None:
288
365
  """Start the node server, the tunnel and the heartbeat."""
@@ -50,6 +50,35 @@ def refresh_connection(creds: Credentials) -> bool:
50
50
  return True
51
51
 
52
52
 
53
+ def post_event(
54
+ creds: Credentials,
55
+ event_type: str,
56
+ payload: Any,
57
+ agent: str | None = None,
58
+ ) -> dict[str, Any]:
59
+ """``POST /api/v1/runtimes/{id}/events``; returns the response body.
60
+
61
+ Raises ``httpx.HTTPStatusError`` with the API's message on refusal.
62
+ """
63
+ body: dict[str, Any] = {"event_type": event_type, "payload": payload}
64
+ if agent is not None:
65
+ body["agent"] = agent
66
+ with _client() as client:
67
+ resp = client.post(
68
+ f"{creds.api_url}/api/v1/runtimes/{creds.runtime_id}/events",
69
+ json=body,
70
+ headers={"Authorization": f"Bearer {creds.secret}"},
71
+ )
72
+ if resp.status_code >= 400:
73
+ raise httpx.HTTPStatusError(
74
+ _error_message(resp), request=resp.request, response=resp
75
+ )
76
+ data = resp.json()
77
+ if not isinstance(data, dict):
78
+ raise ValueError("unexpected response from the platform")
79
+ return data
80
+
81
+
53
82
  def register_runtime(
54
83
  api_url: str, api_key: str, body: dict[str, Any]
55
84
  ) -> dict[str, Any]:
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "levain-runtime"
3
- version = "0.1.2"
3
+ version = "0.1.3"
4
4
  description = "Self-hosted runtime for Levain agents: run an agent's nodes on your own machine or container while the platform drives the recipe"
5
5
  readme = "README.md"
6
6
  license = "Apache-2.0"
File without changes
File without changes