levain-runtime 0.1.2__tar.gz → 0.1.4__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.4
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
@@ -90,6 +90,15 @@ def server_url(gateway_url: str) -> str:
90
90
  return gateway_url
91
91
 
92
92
 
93
+ def gateway(creds: Credentials) -> str:
94
+ """The gateway to dial: the platform's, unless this host overrides it.
95
+
96
+ A runner on the platform cluster's own node dials the gateway Service
97
+ directly instead of looping out through the public edge and back.
98
+ """
99
+ return os.environ.get("LEVAIN_RUNTIME_GATEWAY_URL") or creds.gateway_url
100
+
101
+
93
102
  def client_argv(binary: Path, creds: Credentials) -> list[str]:
94
103
  remotes = [f"R:{creds.reverse_port}:127.0.0.1:{RUNNER_HTTP_PORT}"]
95
104
  remotes += [f"{f.local_port}:{f.remote}" for f in creds.forwards]
@@ -102,7 +111,7 @@ def client_argv(binary: Path, creds: Credentials) -> list[str]:
102
111
  # default lets the reconnect wait grow to five minutes.
103
112
  "--max-retry-interval",
104
113
  "30s",
105
- server_url(creds.gateway_url),
114
+ server_url(gateway(creds)),
106
115
  *remotes,
107
116
  ]
108
117
 
@@ -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."""
@@ -305,7 +382,7 @@ def status() -> None:
305
382
  )
306
383
  if config.credentials:
307
384
  print(f"runtime: {config.credentials.runtime_id}")
308
- print(f"gateway: {config.credentials.gateway_url}")
385
+ print(f"gateway: {chisel.gateway(config.credentials)}")
309
386
  else:
310
387
  print("runtime: not connected")
311
388
  if config.mode == "direct":
@@ -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.4"
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