withruntime 0.6.0 → 0.8.0

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c855551b3670978d87aab0540d48736b4f9f0935cb8109cbb9a6bdbf85b05d96
4
- data.tar.gz: c2e545740a78cc37fdcdec6de993c4b12402906f68a0bd3692d07f357bd17c4e
3
+ metadata.gz: b82967a1882d29df57dcef740f00077ce3a9afd05a1fdb6b031122438adfb92a
4
+ data.tar.gz: d79d6dc698f67d2e8fb8ee287f9830c243607cb7e127935ec6df6b21b2f2d736
5
5
  SHA512:
6
- metadata.gz: 9ada037d0a4b3ddc819067452b0688e027df4ffdc2378de1dcc96a509e1aa0243ce8f58e2d00b458861d6b15afc27d1e708a0f03181fbbfb2a016e5f4691c1be
7
- data.tar.gz: 316571ca7aae8d6714b2998e8a78b15a76ba2de7eb6c6a638efcb7652db98ff59435a9b05146e74e406900c7639176557ee1912a36578eedc7b0109668ea086d
6
+ metadata.gz: d3f846d1588e641fa9e781707be27af354a9b474e808b5657c918c6792a468b3cf49a08ea1eed6ecfe98591bec3d6935b3ff964c3382a8863ee143665ad936d0
7
+ data.tar.gz: 8839301368e7bf649663864afba4a313c7c0936b6c228888a6eb3d4ed72a517410502cf231f1f6e9da34d89c3ae8905fcb23307446700a68453a3d4e530bdae0
@@ -10,12 +10,44 @@ module WithRuntime
10
10
  end
11
11
 
12
12
  # One entry of a session's log: +seq+, +kind+ (input, answer, reply, ask,
13
- # status, result, error or note), +actor+, +body+ and +created_at+. +text+
14
- # is its words, +body.text+.
13
+ # status, result, error, note, or child: a child session's end, posted into
14
+ # its parent's log), +actor+, +body+ and +created_at+. +text+ is its words,
15
+ # +body.text+.
15
16
  class AgentEvent < Record
16
17
  def text = self["body"].is_a?(Hash) ? self["body"]["text"] : nil
17
18
  end
18
19
 
20
+ # A session as the API answers it. Times are ISO 8601 strings.
21
+ #
22
+ # +id+, +agent_id+, +agent_version+, +key+, +trigger+ (api, slack, github,
23
+ # schedule, webhook, or spawn: a child its parent's harness started),
24
+ # +state+ (queued, running, waiting, done, failed or stopped), +reason+
25
+ # (why it ended; among others "budget", "parent_ended", and "deleted" on a
26
+ # deleted record), +result+, +sandbox_id+, +last_seq+, +labels+,
27
+ # +created_at+, +started_at+, +ended_at+.
28
+ #
29
+ # Its tree: +parent_session_id+ (nil for a top session), +root_session_id+
30
+ # (itself for a top session) and +depth+ (0 to 3). Its use: +cost+ (this
31
+ # session's so far) and +tree_cost+ (it and every session under it), each
32
+ # an AgentCost. Its storage: +history_bytes+ (its log, kept until the
33
+ # session is deleted) and +attachment_bytes+ (its files and screenshots).
34
+ # +stop_why+ is why a stop was asked ("stopped", "budget" or
35
+ # "parent_ended") or nil, and +deleted_at+ is set once its history was
36
+ # deleted (+sessions.delete+).
37
+ class AgentSessionInfo < Record
38
+ def cost = AgentCost.new(self["cost"])
39
+ def tree_cost = AgentCost.new(self["treeCost"])
40
+ def deleted? = !self["deletedAt"].nil?
41
+ end
42
+
43
+ # What a session, or its tree, has used: +dollars+, +vcpu_seconds+ and
44
+ # +gib_seconds+ (0 when not given).
45
+ class AgentCost < Record
46
+ def dollars = self["dollars"] || 0
47
+ def vcpu_seconds = self["vcpuSeconds"] || 0
48
+ def gib_seconds = self["gibSeconds"] || 0
49
+ end
50
+
19
51
  # +runtime.agents+: agents that run in their own sandbox per session, started
20
52
  # by your own app through the API, Slack, GitHub, a schedule or a webhook. A private
21
53
  # beta: an account outside it is refused +product_in_beta+
@@ -28,14 +60,18 @@ module WithRuntime
28
60
  #
29
61
  # A definition is a Hash, by snake_case keyword (+memory_mib:+) or by the
30
62
  # API's own key ("memoryMiB"): +command+ (required), +from+ ({ image: } or
31
- # { snapshot: }), +vcpu+ and +memory_mib+ (4 vCPU and 8 GiB unless set),
32
- # +instructions+, +env+, +secrets+ (names of egress secrets: model keys go
33
- # here), +mcp+ ([{ name:, url: }]), +volumes+ ([{ volume:, path: }]),
34
- # +idle_pause_seconds+, +max_session_seconds+, +max_concurrent+,
35
- # +max_session_dollars+ (5 unless set: a session at it stops, reason
36
- # "budget"), +max_day_dollars+ (100 unless set: an agent at it starts nothing
37
- # new, ConflictError +budget_reached+, until the next day) and +template+
38
- # ("support" or "team").
63
+ # { snapshot: }), +vcpu+ and +memory_mib+ (4 vCPU and 8 GiB unless set; a
64
+ # definition from a snapshot takes neither, InvalidRequestError, since a
65
+ # snapshot keeps its own size), +instructions+, +env+, +secrets+ (names of
66
+ # egress secrets: model keys go here), +mcp+ ([{ name:, url: }]), +volumes+
67
+ # ([{ volume:, path: }]), +idle_pause_seconds+, +max_session_seconds+,
68
+ # +max_concurrent+, +max_session_dollars+ (more than 0, at most 10000, 5
69
+ # unless set: the session and every session under it, the root's covering
70
+ # its tree; a tree at it stops, reason "budget"), +max_day_dollars+ (more
71
+ # than 0, at most 100000, 100 unless set: the agent's sessions started that
72
+ # UTC day; an agent at it starts nothing new, ConflictError
73
+ # +budget_reached+, until the next day) and +template+ ("support" or
74
+ # "team").
39
75
  class Agents < Product
40
76
  attr_reader :sessions, :triggers, :templates
41
77
 
@@ -99,37 +135,42 @@ module WithRuntime
99
135
  AgentSession.new(@t, info, joined: false, seq: info["lastSeq"])
100
136
  end
101
137
 
102
- # Deletes an ended session and its history; until then it is kept,
103
- # counted as storage. An open session is refused +session_open+
104
- # (ConflictError): stop it first.
138
+ # Deletes an ended session's history: its log, and every log under it in
139
+ # its tree. Until then the history is kept, counted as storage. The
140
+ # session is kept as a deleted record: +get+ still reads it, with
141
+ # +deleted_at+ set, +reason+ "deleted", +key+ and +result+ cleared and its
142
+ # cost kept (a delete never frees budget), and its log reads empty. An
143
+ # open session or tree is refused +session_open+ (ConflictError): stop it
144
+ # first.
105
145
  def delete(id) = @t.json("DELETE", "/v1/agents/sessions/#{seg(id)}") && nil
106
146
 
107
- # The first page of sessions, as records; +each+ walks every page.
108
- # +agent+ is an id or a name; +state+ is queued, running, waiting, done,
109
- # failed or stopped; at most +limit+ a page.
110
- def list(agent: nil, state: nil, cursor: nil, limit: nil)
111
- sessions_page({ "agent" => agent, "state" => state, "limit" => limit }, cursor)
147
+ # The first page of sessions, as AgentSessionInfo records; +each+ walks
148
+ # every page. +agent+ is an id or a name; +state+ is queued, running,
149
+ # waiting, done, failed or stopped; +parent+ (a session's id) lists its
150
+ # direct children; +root+ (a top session's id) its whole tree, the root
151
+ # included; +deleted: true+ includes deleted records, hidden otherwise; at
152
+ # most +limit+ a page.
153
+ def list(agent: nil, state: nil, parent: nil, root: nil, deleted: nil, cursor: nil, limit: nil)
154
+ query = { "agent" => agent, "state" => state, "parent" => parent, "root" => root, "deleted" => deleted, "limit" => limit }
155
+ sessions_page(query, cursor)
112
156
  end
113
157
 
114
158
  private
115
159
 
116
160
  def sessions_page(query, cursor)
117
161
  body = @t.json("GET", "/v1/agents/sessions", query: query.merge("cursor" => cursor))
118
- Page.new(body["data"].map { |item| Record.new(item) }, body["nextCursor"]) { |after| sessions_page(query, after) }
162
+ Page.new(body["data"].map { |item| AgentSessionInfo.new(item) }, body["nextCursor"]) { |after| sessions_page(query, after) }
119
163
  end
120
164
  end
121
165
 
122
- # A session of an agent. +info+ is its latest record: +state+ (queued,
123
- # running, waiting, done, failed or stopped), +reason+, +result+,
124
- # +last_seq+ and the rest; once the API sends them, +parent_session_id+ and
125
- # +root_session_id+ (a child session's parent and its tree's root), +cost+
126
- # ({ dollars, vcpuSeconds, gibSeconds } so far), +tree_cost+ (its children's
127
- # summed in), +history_bytes+ (text history, counted as storage) and
128
- # +attachment_bytes+ (files and screenshots, counted as storage), each nil
129
- # until then; history is kept until the session is deleted
130
- # (+sessions.delete+). +joined+ is whether +start+
131
- # joined an open session with the same key, and +seq+ the number of the
132
- # words it sent.
166
+ # A session of an agent. +info+ is its latest record, an AgentSessionInfo:
167
+ # +state+ (queued, running, waiting, done, failed or stopped), +reason+,
168
+ # +result+, +last_seq+, its tree (+parent_session_id+, +root_session_id+,
169
+ # +depth+), its use (+cost+, +tree_cost+), its storage (+history_bytes+,
170
+ # +attachment_bytes+), +stop_why+, +deleted_at+ and the rest; history is
171
+ # kept until the session is deleted (+sessions.delete+). +joined+ is
172
+ # whether +start+ joined an open session with the same key, and +seq+ the
173
+ # number of the words it sent.
133
174
  class AgentSession
134
175
  ENDED = %w[done failed stopped].freeze
135
176
  # How long the API holds an events read open, and the margin past it a
@@ -141,7 +182,7 @@ module WithRuntime
141
182
 
142
183
  def initialize(transport, info, joined: false, seq: nil)
143
184
  @t = transport
144
- @info = Record.new(info)
185
+ @info = AgentSessionInfo.new(info)
145
186
  @joined = joined
146
187
  @seq = seq
147
188
  end
@@ -153,10 +194,15 @@ module WithRuntime
153
194
 
154
195
  # Reads the session again.
155
196
  def refresh(timeout: nil)
156
- @info = Record.new(@t.json("GET", path, timeout: timeout))
197
+ @info = AgentSessionInfo.new(@t.json("GET", path, timeout: timeout))
157
198
  self
158
199
  end
159
200
 
201
+ # The first page of its direct children, the sessions its harness started
202
+ # (+trigger+ "spawn"), newest first; +each+ walks every page.
203
+ # +sessions.list(root:)+ reads a whole tree.
204
+ def children(cursor: nil, limit: nil) = AgentSessions.new(@t).list(parent: id, cursor: cursor, limit: limit)
205
+
160
206
  # Sends words into a running or waiting session and answers their seq.
161
207
  # The idempotency key, made here when not given, means a retried send is
162
208
  # never two messages. Named send_message, not send, so Ruby's own
@@ -192,7 +238,7 @@ module WithRuntime
192
238
  # Asks the session to stop and keeps the answer. A running session ends
193
239
  # once its sandbox is deleted; this does not wait for that (+wait+ does).
194
240
  def stop
195
- @info = Record.new(@t.json("POST", path("/stop"), body: {}))
241
+ @info = AgentSessionInfo.new(@t.json("POST", path("/stop"), body: {}))
196
242
  end
197
243
 
198
244
  # The session once it has ended (done, failed or stopped); a failed
@@ -25,7 +25,7 @@ module WithRuntime
25
25
  class Client
26
26
  attr_reader :transport, :sandboxes, :snapshots, :images, :volumes, :secrets, :limits, :emails, :referrals,
27
27
  :switching, :events, :webhooks, :otel, :audit, :feedback, :support, :domains, :ports, :ips, :networks, :sso, :mcp, :agents,
28
- :apps, :functions, :builds
28
+ :apps, :functions, :builds, :containers
29
29
 
30
30
  # +api_key+: default RUNTIME_API_KEY, then this machine's saved connection.
31
31
  # +base_url+: default RUNTIME_API_URL, then https://api.withruntime.com.
@@ -77,6 +77,7 @@ module WithRuntime
77
77
  @apps = Apps.new(@transport, "/v1/apps")
78
78
  @functions = Apps.new(@transport, "/v1/functions")
79
79
  @builds = Builds.new(@transport)
80
+ @containers = Containers.new(@transport)
80
81
  end
81
82
 
82
83
  # Renamed on 6 October 2026, one plural word per product: each old name is the product it became, with a warning,
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WithRuntime
4
+ # +runtime.containers+: one OCI image's process on its own microVM, billed
5
+ # at the sandbox rates, at /v1/containers. In a private beta: an account
6
+ # outside it is refused with product_in_beta (403, PermissionDeniedError).
7
+ #
8
+ # box = runtime.containers.create(image: "nginx:1.27", vcpu: 1, memory_mib: 512, name: "web")
9
+ # box = runtime.containers.wait(box.id, "running")
10
+ # puts box.port
11
+ #
12
+ # A container: id, name, labels, state (waiting, running, sleeping, stopped,
13
+ # failed, deleting, deleted), image (pinned to its digest), entrypoint,
14
+ # command, working_dir, user, port (or nil), exposed_ports, on_exit, sleep,
15
+ # vcpu, memory_mib, env_names (the environment's names, never its values),
16
+ # error ({code, message} or nil), created_at, updated_at and deleted_at.
17
+ class Containers < ResourceProduct
18
+ def initialize(transport)
19
+ super(transport, "/v1/containers", %w[failed deleted])
20
+ end
21
+
22
+ # Runs one copy of +image:+ (an OCI reference; the answer pins its digest)
23
+ # with +vcpu:+ (1 to 16) and +memory_mib:+ (128 to 65536), all required.
24
+ # +entrypoint:+ replaces the image's Entrypoint and drops its Cmd;
25
+ # +command:+ replaces its Cmd. +env:+ is set over the image's own and never
26
+ # read back, only its names. +port:+ (1 to 65535) is required when the
27
+ # image exposes several TCP ports. +on_exit:+ is "restart" or "stop": by
28
+ # default it restarts when it serves a port, else stops. It never sleeps
29
+ # unless +sleep: true+. Also +name:+, +labels:+, +working_dir:+ and +user:+.
30
+ def create(idempotency_key: nil, **fields) = super
31
+
32
+ # Only +name:+ and +labels:+ change; +name: :remove+ clears the name.
33
+ def update(id, idempotency_key: nil, **changes) = super
34
+
35
+ # Follows its state to +target+ ("running", say); reaching failed or
36
+ # deleted first raises with its error.
37
+ def wait(id, target, timeout: 300, poll: 0.25) = super
38
+ end
39
+ end
@@ -25,6 +25,12 @@ module WithRuntime
25
25
  # room, the create raises +no_capacity+ and nothing is made or charged.
26
26
  # +sbx.info.placement+ is an opaque tag for its server: the same tag means
27
27
  # the same server.
28
+ #
29
+ # +secrets: [name, ...]+ is the egress secrets this sandbox may use, by
30
+ # name: only those of your egress secrets that would otherwise reach it.
31
+ # +[]+ is none; leave it out for every secret that reaches it. Fixed for
32
+ # the sandbox's life; a copy of a snapshot may only narrow its source's
33
+ # list. +sbx.info.secrets+ reads it back, nil for every secret.
28
34
  def create(wait: true, idempotency_key: nil, wait_for_capacity: nil, **fields)
29
35
  room = wait_for_capacity.nil? ? @t.wait_for_capacity : [0, wait_for_capacity].max
30
36
  info = @t.json("POST", "/v1/sandboxes", body: Fields.body(fields), wait: wait ? 60 : 0,
@@ -208,8 +214,14 @@ module WithRuntime
208
214
  # run. One copy without +count+; an array with it. If a copy fails, the
209
215
  # error's +details["startedSandboxIds"]+ names the copies that did start.
210
216
  # +funding+ is accepted and ignored: copies run on free usage first, then credit.
211
- def fork(count: nil, name: nil, labels: nil, keep_snapshot: nil, funding: nil, idempotency_key: nil)
212
- body = Fields.body(count: count, name: name, labels: labels, keep_snapshot: keep_snapshot)
217
+ # +secrets: [name, ...]+ is the egress secrets the copies may use, by name:
218
+ # only those of your egress secrets that would otherwise reach them. +[]+
219
+ # is none; leave it out to keep this sandbox's list. Fixed for the copies'
220
+ # life; it may only narrow this sandbox's list.
221
+ def fork(count: nil, name: nil, labels: nil, keep_snapshot: nil, funding: nil, secrets: nil,
222
+ idempotency_key: nil)
223
+ body = Fields.body(count: count, name: name, labels: labels, keep_snapshot: keep_snapshot,
224
+ secrets: secrets&.to_a)
213
225
  reply = @t.json("POST", path(":fork"), body: body, wait: 60, key: idempotency_key)
214
226
  copies = reply["sandboxes"].map { |info| Sandbox.new(@t, info) }
215
227
  count.nil? ? copies.first : copies
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module WithRuntime
4
- VERSION = "0.6.0"
4
+ VERSION = "0.8.0"
5
5
  end
data/lib/withruntime.rb CHANGED
@@ -30,6 +30,7 @@ require_relative "withruntime/agents"
30
30
  require_relative "withruntime/images"
31
31
  require_relative "withruntime/apps"
32
32
  require_relative "withruntime/builds"
33
+ require_relative "withruntime/containers"
33
34
  require_relative "withruntime/network_products"
34
35
  require_relative "withruntime/parity_products"
35
36
  require_relative "withruntime/file_watch"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: withruntime
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.0
4
+ version: 0.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Runtime
@@ -25,6 +25,7 @@ files:
25
25
  - lib/withruntime/apps.rb
26
26
  - lib/withruntime/builds.rb
27
27
  - lib/withruntime/client.rb
28
+ - lib/withruntime/containers.rb
28
29
  - lib/withruntime/credentials.rb
29
30
  - lib/withruntime/errors.rb
30
31
  - lib/withruntime/fields.rb