tama-rb 0.1.2

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: a8e4b7c980bd24d10783272afb5e38e76a8863d6879d849cc9b62f00493abbab
4
+ data.tar.gz: 786b9d8c94414e52f0f8b9879317de943bdb2d59bd5dc74a730c9e7b78bc2f04
5
+ SHA512:
6
+ metadata.gz: 88083d85c2da41b50c4c4bd231005e53e94f05222235dcd26817dfb23d92fbbc80bd6645c4c59dd2bb7c8c96dbe908c293d726466605e1e4ccb968e7546b3717
7
+ data.tar.gz: 340e80ecebb7cefad5e2e6341e82e740a116e76bb989fc6f342aa3baa8e27b7fdd47d55624c9f92300686573c728372687fcfc84b44cde9df001e59f2c8e36ec
data/CHANGELOG.md ADDED
@@ -0,0 +1,11 @@
1
+ # Changelog
2
+
3
+ ## 0.1.1 - 2026-08-21
4
+
5
+ - Fix the Trusted Publishing workflow and align release tags with Bundler's `v` prefix convention.
6
+
7
+ ## 0.1.0 - 2026-08-21
8
+
9
+ - Initial release with Neural, Memory, Perception, and Agentic services.
10
+ - Add strict immutable models and the complete Broadcast parser hierarchy.
11
+ - Add typed errors, retry support, path encoding, and incremental SSE parsing.
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Zack Siri
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,228 @@
1
+ # Tama Ruby
2
+
3
+ `tama-rb` is the Ruby client for Tama APIs. It provides namespace-aware Faraday services, strict immutable response models, typed errors, and incremental server-sent event parsing.
4
+
5
+ ## Installation
6
+
7
+ Add the gem to your bundle:
8
+
9
+ ```ruby
10
+ gem "tama-rb", "~> 0.1.0"
11
+ ```
12
+
13
+ Then run `bundle install` and load the client with:
14
+
15
+ ```ruby
16
+ require "tama"
17
+ ```
18
+
19
+ Ruby 3.2 or newer is required.
20
+
21
+ ## Client Setup
22
+
23
+ Create a client with the base URL for the API namespace being used:
24
+
25
+ ```ruby
26
+ client = Tama::Client.new(
27
+ base_url: "https://api.example.com/provision",
28
+ headers: {"Authorization" => "Bearer token"},
29
+ timeout: 300,
30
+ retries: 2
31
+ )
32
+ ```
33
+
34
+ Timeouts are expressed in seconds. Global headers are merged with per-request headers, with per-request values taking precedence. Retries use `faraday-retry` for retryable requests and statuses. The default transport is Faraday's Net::HTTP adapter.
35
+
36
+ An existing connection can be injected when custom Faraday middleware or an adapter is needed. The injected connection is used as configured; `base_url` is still required for request URL construction and namespace validation.
37
+
38
+ ```ruby
39
+ connection = Faraday.new do |faraday|
40
+ faraday.request :json
41
+ faraday.request :retry, max: 3
42
+ faraday.adapter :net_http
43
+ end
44
+
45
+ client = Tama::Client.new(
46
+ base_url: "https://api.example.com/perception",
47
+ connection: connection
48
+ )
49
+ ```
50
+
51
+ ### Namespace URLs
52
+
53
+ The final base URL path segment is validated for each operation. Prefixes such as `/api/v1` are preserved.
54
+
55
+ | Namespace | Operations |
56
+ | --- | --- |
57
+ | `provision` | Neural space/class/operation, perception chain lookup |
58
+ | `ingest` | Memory entity creation |
59
+ | `perception` | Perception concept listing |
60
+ | `agentic` | Agentic message creation and streaming |
61
+
62
+ Use separate clients when an application calls multiple namespaces.
63
+
64
+ ## Neural
65
+
66
+ ```ruby
67
+ client = Tama::Client.new(base_url: "https://api.example.com/provision")
68
+
69
+ space = client.neural.get_space("my-space")
70
+ klass = client.neural.get_class(space, "article")
71
+
72
+ operation = client.neural.create_class_operation(
73
+ klass,
74
+ chain_ids: ["chain-1", "chain-2"],
75
+ node_type: "compute"
76
+ )
77
+ ```
78
+
79
+ `get_class` accepts a `Tama::Neural::Space` or a space ID. `create_class_operation` accepts a `Tama::Neural::Klass` or class ID and either a hash or `Tama::Neural::OperationParams`.
80
+
81
+ ## Memory
82
+
83
+ ```ruby
84
+ client = Tama::Client.new(base_url: "https://api.example.com/ingest")
85
+
86
+ entity = client.memory.create_entity(
87
+ klass,
88
+ identifier: "article-123",
89
+ record: {title: "Tama with Ruby"},
90
+ validate_record: true
91
+ )
92
+ ```
93
+
94
+ `validate_record` defaults to `true`. A `Tama::Memory::EntityParams` object can be passed instead of a hash.
95
+
96
+ ## Perception
97
+
98
+ Chain lookup uses a provision client:
99
+
100
+ ```ruby
101
+ client = Tama::Client.new(base_url: "https://api.example.com/provision")
102
+ chain = client.perception.get_chain(space, "summarize")
103
+ ```
104
+
105
+ `get_chain` accepts a `Tama::Neural::Space` or space ID. Concept listing uses a perception client and supports query parameters:
106
+
107
+ ```ruby
108
+ client = Tama::Client.new(base_url: "https://api.example.com/perception")
109
+
110
+ concepts = client.perception.list_concepts(
111
+ "entity-123",
112
+ query: {limit: 20, offset: 0, relation: "reply"}
113
+ )
114
+ ```
115
+
116
+ ## Agentic
117
+
118
+ ```ruby
119
+ client = Tama::Client.new(base_url: "https://api.example.com/agentic")
120
+
121
+ message = client.agentic.create_message(
122
+ recipient: "user-123",
123
+ identifier: "message-123",
124
+ content: "Hello",
125
+ index: 1,
126
+ author: {identifier: "agent-1", source: "system"},
127
+ thread: {identifier: "thread-1"}
128
+ )
129
+ ```
130
+
131
+ Message class defaults are `user-message`, `actor`, and `thread`. A `Tama::Agentic::MessageParams` object can also be supplied.
132
+
133
+ ### Streaming
134
+
135
+ Streaming requires either `callback:` or a block. Each handler call receives one decoded JSON event. The parser buffers fragmented chunks, supports LF and CRLF separators, multiline `data:` fields, multiple events per chunk, and `[DONE]`.
136
+
137
+ ```ruby
138
+ response = client.agentic.create_message(message_attributes, stream: true) do |event|
139
+ puts event.inspect
140
+ end
141
+ ```
142
+
143
+ The streaming form returns the successful `Faraday::Response`. Non-streaming creation parses the API's top-level JSON response into a `Tama::Agentic::Message`.
144
+
145
+ ## Request Options
146
+
147
+ Every operation accepts `headers:` and `timeout:` request options where applicable:
148
+
149
+ ```ruby
150
+ space = client.neural.get_space(
151
+ "my-space",
152
+ headers: {"X-Request-ID" => "request-1"},
153
+ timeout: 30
154
+ )
155
+ ```
156
+
157
+ Dynamic path segments are percent-encoded. Query parameters should be passed only through `query:` on `list_concepts`.
158
+
159
+ ## Models
160
+
161
+ Response and parser models are immutable Ruby `Data` objects:
162
+
163
+ - `Tama::Neural::Space`, `Tama::Neural::Klass`, `Tama::Neural::Operation`, `Tama::Neural::OperationParams`
164
+ - `Tama::Memory::Entity`, `Tama::Memory::EntityParams`
165
+ - `Tama::Perception::Chain`, `Tama::Perception::Concept`, `Tama::Perception::Generator`
166
+ - `Tama::Agentic::Message`, `Tama::Agentic::MessageParams`, `Tama::Agentic::Author`, `Tama::Agentic::Thread`
167
+ - `Tama::Broadcast` and `Tama::Broadcast::Event`, `Metadata`, `Step`, `Concept`, `Thought`, `Chain`, `Branch`, `Flow`, `OriginEntity`
168
+
169
+ Broadcast payloads are parsed without making an HTTP request. Top-level `event` and `step` values are required:
170
+
171
+ ```ruby
172
+ broadcast = Tama::Broadcast.parse(
173
+ event: {name: "step.updated", domain: "workflow"},
174
+ step: {id: "step-1", concepts: []}
175
+ )
176
+ ```
177
+
178
+ Response models also expose `.parse(hash_or_json)`. Invalid JSON, required fields, types, enums, or nested models raise rather than creating partial empty models.
179
+
180
+ ## Errors
181
+
182
+ Failures raise subclasses of `Tama::Error`:
183
+
184
+ - `Tama::Error::ConfigurationError` for malformed client configuration
185
+ - `Tama::Error::InvalidNamespaceError` for a mismatched final base URL segment
186
+ - `Tama::Error::ValidationError` for local parameters and HTTP 422 responses; inspect `errors`
187
+ - `Tama::Error::NotFoundError` for HTTP 404 responses
188
+ - `Tama::Error::HTTPError` for other non-2xx responses; inspect `status` and `body`
189
+ - `Tama::Error::TransportError` for Faraday transport failures; inspect `cause`
190
+ - `Tama::Error::ParseError` for malformed JSON, response envelopes, models, or SSE events
191
+
192
+ All HTTP statuses from 200 through 299 are successful. Neural, Memory, and Perception model endpoints require the API's exact `{ "data": ... }` response envelope; Agentic message creation uses its top-level response object.
193
+
194
+ ## Development
195
+
196
+ ```sh
197
+ bin/setup
198
+ bundle exec rspec
199
+ bundle exec standardrb
200
+ bundle exec rake build
201
+ ```
202
+
203
+ `bin/console` starts an IRB session with Tama loaded.
204
+
205
+ ## Releasing
206
+
207
+ Releases use RubyGems Trusted Publishing, so the repository does not store a long-lived RubyGems API key.
208
+
209
+ Before the first release, create a pending trusted publisher at <https://rubygems.org/profile/oidc/pending_trusted_publishers> with:
210
+
211
+ - Gem name: `tama-rb`
212
+ - Repository owner: `upmaru`
213
+ - Repository name: `tama-rb`
214
+ - Workflow filename: `publish.yml`
215
+ - GitHub environment: `release`
216
+
217
+ Create the `release` environment in the GitHub repository settings. After updating `Tama::VERSION` and `CHANGELOG.md`, publish by pushing a matching version tag:
218
+
219
+ ```sh
220
+ git tag v0.1.0
221
+ git push origin v0.1.0
222
+ ```
223
+
224
+ The first successful workflow run publishes the gem and converts the pending publisher into a trusted publisher. RubyGems organizations are currently in private beta; organization ownership requires beta access followed by transferring the published gem to the organization.
225
+
226
+ ## License
227
+
228
+ The gem is available under the MIT License.
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tama
4
+ module Agentic
5
+ Author = Data.define(:identifier, :source, :klass) do
6
+ def initialize(identifier:, source:, klass: "actor")
7
+ Params.require_string!({"identifier" => identifier}, "identifier")
8
+ Params.require_string!({"source" => source}, "source")
9
+ Params.require_string!({"class" => klass}, "class")
10
+ super
11
+ end
12
+
13
+ def self.parse(value)
14
+ data = Params.hash(value, "author")
15
+ new(identifier: data["identifier"], source: data["source"], klass: data.fetch("class", "actor"))
16
+ end
17
+
18
+ def to_h
19
+ {"identifier" => identifier, "source" => source, "class" => klass}
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tama
4
+ module Agentic
5
+ Message = Model.define(
6
+ :id, :status, :message, :identifier, :current_state,
7
+ required: %i[id],
8
+ types: {id: String, status: String, message: Hash, identifier: String, current_state: String}
9
+ )
10
+ end
11
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tama
4
+ module Agentic
5
+ MessageParams = Data.define(:recipient, :klass, :identifier, :content, :index, :author, :thread) do
6
+ def initialize(recipient:, identifier:, content:, index:, author:, thread:, klass: "user-message")
7
+ Params.require_string!({"recipient" => recipient}, "recipient")
8
+ Params.require_string!({"class" => klass}, "class")
9
+ Params.require_string!({"identifier" => identifier}, "identifier")
10
+ Params.require_string!({"content" => content}, "content")
11
+ unless index.is_a?(Integer)
12
+ raise Error::ValidationError.new("index is required", errors: {index: "must be an integer"})
13
+ end
14
+ raise Error::ValidationError, "author is invalid" unless author.is_a?(Author)
15
+ raise Error::ValidationError, "thread is invalid" unless thread.is_a?(Thread)
16
+
17
+ super
18
+ end
19
+
20
+ def self.parse(value)
21
+ data = Params.hash(value, "message")
22
+ new(
23
+ recipient: data["recipient"],
24
+ klass: data.fetch("class", "user-message"),
25
+ identifier: data["identifier"],
26
+ content: data["content"],
27
+ index: data["index"],
28
+ author: Author.parse(data["author"]),
29
+ thread: Thread.parse(data["thread"])
30
+ )
31
+ end
32
+
33
+ def to_h
34
+ {
35
+ "recipient" => recipient,
36
+ "class" => klass,
37
+ "identifier" => identifier,
38
+ "content" => content,
39
+ "index" => index,
40
+ "author" => author.to_h,
41
+ "thread" => thread.to_h
42
+ }
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tama
4
+ module Agentic
5
+ class Service
6
+ def initialize(http)
7
+ @http = http
8
+ end
9
+
10
+ def create_message(attributes = nil, stream: false, callback: nil, headers: {}, timeout: nil, **attribute_keywords, &block)
11
+ @http.validate_namespace!("agentic")
12
+ attributes ||= attribute_keywords
13
+ params = attributes.is_a?(MessageParams) ? attributes : MessageParams.parse(attributes)
14
+ handler = callback || block
15
+
16
+ if stream
17
+ raise Error::ValidationError, "A callback or block is required when streaming" unless handler.respond_to?(:call)
18
+
19
+ create_stream(params, handler, headers:, timeout:)
20
+ else
21
+ response = @http.request(
22
+ :post,
23
+ "messages",
24
+ body: {"stream" => false, "message" => params.to_h},
25
+ headers:,
26
+ timeout:
27
+ )
28
+ @http.parse_body(response, Message)
29
+ end
30
+ end
31
+
32
+ private
33
+
34
+ def create_stream(params, handler, headers:, timeout:)
35
+ parser = SSEParser.new(handler)
36
+ stream_headers = {"Accept" => "text/event-stream"}.merge(headers)
37
+ response = @http.request(
38
+ :post,
39
+ "messages",
40
+ body: {"stream" => true, "message" => params.to_h},
41
+ headers: stream_headers,
42
+ timeout:
43
+ ) { |chunk, *| parser.feed(chunk) }
44
+ @http.check_status!(response)
45
+ parser.finish
46
+ response
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tama
4
+ module Agentic
5
+ Thread = Data.define(:identifier, :klass) do
6
+ def initialize(identifier:, klass: "thread")
7
+ Params.require_string!({"identifier" => identifier}, "identifier")
8
+ Params.require_string!({"class" => klass}, "class")
9
+ super
10
+ end
11
+
12
+ def self.parse(value)
13
+ data = Params.hash(value, "thread")
14
+ new(identifier: data["identifier"], klass: data.fetch("class", "thread"))
15
+ end
16
+
17
+ def to_h
18
+ {"identifier" => identifier, "class" => klass}
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tama
4
+ Broadcast = Model.define(
5
+ :event, :step,
6
+ required: %i[event step],
7
+ transforms: {
8
+ event: ->(value) { Broadcast::Event.parse(value) },
9
+ step: ->(value) { Broadcast::Step.parse(value) }
10
+ }
11
+ )
12
+
13
+ class Broadcast
14
+ Metadata = Model.define(
15
+ :changes, :comment, :parameters,
16
+ types: {changes: Hash, comment: String, parameters: Hash}
17
+ )
18
+
19
+ Event = Model.define(
20
+ :name, :domain, :metadata,
21
+ types: {name: String, domain: String, metadata: Metadata},
22
+ transforms: {metadata: ->(value) { Metadata.parse(value) }}
23
+ )
24
+
25
+ Concept = Model.define(
26
+ :id, :relation, :content,
27
+ types: {id: String, relation: String, content: Hash}
28
+ )
29
+
30
+ Chain = Model.define(
31
+ :id, :name,
32
+ types: {id: String, name: String}
33
+ )
34
+
35
+ Thought = Model.define(
36
+ :chain, :relation, :index,
37
+ types: {chain: Chain, relation: String, index: Integer},
38
+ transforms: {chain: ->(value) { Chain.parse(value) }}
39
+ )
40
+
41
+ OriginEntity = Model.define(
42
+ :id, :current_state, :identifier,
43
+ types: {id: String, current_state: String, identifier: String}
44
+ )
45
+
46
+ Flow = Model.define(
47
+ :id, :origin_entity,
48
+ types: {id: String, origin_entity: OriginEntity},
49
+ transforms: {origin_entity: ->(value) { OriginEntity.parse(value) }}
50
+ )
51
+
52
+ Branch = Model.define(
53
+ :id, :chain_id, :current_state, :flow,
54
+ types: {id: String, chain_id: String, current_state: String, flow: Flow},
55
+ transforms: {flow: ->(value) { Flow.parse(value) }}
56
+ )
57
+
58
+ Step = Model.define(
59
+ :id, :current_state, :index, :attempt, :concepts, :thought, :branch,
60
+ defaults: {concepts: []},
61
+ types: {
62
+ id: String, current_state: String, index: Integer, attempt: Integer,
63
+ concepts: Array, thought: Thought, branch: Branch
64
+ },
65
+ transforms: {
66
+ concepts: lambda do |value|
67
+ raise Error::ValidationError, "concepts must be an array" unless value.is_a?(Array)
68
+
69
+ value.map { |concept| Concept.parse(concept) }
70
+ end,
71
+ thought: ->(value) { Thought.parse(value) },
72
+ branch: ->(value) { Branch.parse(value) }
73
+ }
74
+ )
75
+ end
76
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tama
4
+ class Client
5
+ DEFAULT_TIMEOUT = 300
6
+ DEFAULT_RETRIES = 2
7
+
8
+ attr_reader :neural, :memory, :perception, :agentic, :connection
9
+
10
+ def initialize(base_url:, headers: {}, timeout: DEFAULT_TIMEOUT, retries: DEFAULT_RETRIES, connection: nil)
11
+ http = HTTP.new(base_url:, headers:, timeout:, retries:, connection:)
12
+ @connection = http.connection
13
+ @neural = Neural::Service.new(http)
14
+ @memory = Memory::Service.new(http)
15
+ @perception = Perception::Service.new(http)
16
+ @agentic = Agentic::Service.new(http)
17
+ freeze
18
+ end
19
+ end
20
+ end
data/lib/tama/error.rb ADDED
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tama
4
+ class Error < StandardError
5
+ class ConfigurationError < Error; end
6
+ class InvalidNamespaceError < ConfigurationError; end
7
+
8
+ class ValidationError < Error
9
+ attr_reader :errors, :status
10
+
11
+ def initialize(message = "Validation failed", errors: {}, status: nil)
12
+ @errors = errors
13
+ @status = status
14
+ super(message)
15
+ end
16
+ end
17
+
18
+ class HTTPError < Error
19
+ attr_reader :status, :body
20
+
21
+ def initialize(message = nil, status:, body: nil)
22
+ @status = status
23
+ @body = body
24
+ super(message || "HTTP request failed with status #{status}")
25
+ end
26
+ end
27
+
28
+ class NotFoundError < HTTPError
29
+ def initialize(body: nil)
30
+ super("Resource not found", status: 404, body: body)
31
+ end
32
+ end
33
+
34
+ class TransportError < Error
35
+ attr_reader :cause
36
+
37
+ def initialize(message = "HTTP transport failed", cause: nil)
38
+ @cause = cause
39
+ super(message)
40
+ end
41
+ end
42
+
43
+ class ParseError < Error; end
44
+ end
45
+ end