Skip to content

Commit d76653f

Browse files
authored
establish agent taxonomy - prompter, manifold, automata (#20)
1 parent 2a40f38 commit d76653f

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

61 files changed

+1542
-498
lines changed

.github/workflows/build.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ jobs:
2626
2727
- name: go test
2828
run: |
29+
mkdir -p /tmp/softcmd
30+
mkdir -p /tmp/cmd
2931
go test -v -coverprofile=profile.cov $(go list ./... | grep -v /examples/)
3032
3133
- uses: shogo82148/actions-goveralls@v1

.github/workflows/check-test.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ jobs:
2626
2727
- name: go test
2828
run: |
29+
mkdir -p /tmp/softcmd
30+
mkdir -p /tmp/cmd
2931
go test -v -coverprofile=profile.cov $(go list ./... | grep -v /examples/)
3032
3133
- uses: shogo82148/actions-goveralls@v1

README.md

Lines changed: 76 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -44,10 +44,23 @@ The generative agents autonomously generate output as a reaction on input, past
4444

4545
In this library, an agent is defined as a side-effect function `ƒ: A ⟼ B`, which takes a Golang type `A` as input and autonomously produces an output `B`, while retaining memory of past experiences.
4646

47+
## Design Philosophy
48+
49+
1. **Minimal**: No hidden magic; each component is explicit.
50+
2. **Typed**: Inputs and outputs are type-safe (A, B).
51+
3. **Composable**: Agents can be used standalone, orchestrated by higher layers or piped.
52+
4. **Interoperable**: Supports integration with LLMs (e.g. Bedrock, OpenAI) and tools.
53+
54+
## Getting started
55+
4756
- [Inspiration](#inspiration)
57+
- [Design Philosophy](#design-philosophy)
4858
- [Getting started](#getting-started)
4959
- [Quick example](#quick-example)
5060
- [Agent Architecture](#agent-architecture)
61+
- [Prompter](#prompter)
62+
- [Manifold](#manifold)
63+
- [Automata](#automata)
5164
- [Memory](#memory)
5265
- [Reasoner](#reasoner)
5366
- [Encoder \& Decoder](#encoder--decoder)
@@ -60,9 +73,6 @@ In this library, an agent is defined as a side-effect function `ƒ: A ⟼ B`, wh
6073
- [bugs](#bugs)
6174
- [License](#license)
6275

63-
64-
## Getting started
65-
6676
The latest version of the library is available at `main` branch of this repository. All development, including new features and bug fixes, take place on the `main` branch using forking and pull requests as described in contribution guidelines. The stable version is available via Golang modules.
6777

6878
Running the examples you need access either to AWS Bedrock or OpenAI.
@@ -128,7 +138,68 @@ func main() {
128138

129139
## Agent Architecture
130140

131-
The `thinker` library provides toolkit for running agents with type-safe constraints. It is built on a pluggable architecture, allowing applications to define custom workflows. The diagram below emphasis core building blocks.
141+
The `thinker` framework defines a composable, layered architecture for building AI agents, from simple prompt wrappers to full autonomous systems. Each layer adds capability while preserving modularity and type safety. It's **core abstractions** are:
142+
* **Prompter** Stateless prompt-response unit. Encodes input of type `A`, returns a `chatter.Reply`. No memory or reasoning. Ideal for direct LLM calls or prompt engineering wrappers.
143+
* **Manifold** Single-session agent. Handles structured interactions with optional tool use and ephemeral memory. Encodes `A`, returns structured output `B`. Delegates reasoning to the LLM. Therefore requires sophisticated LLM for reliable operations. (for example [AWS Bedrock models](https://docs.aws.amazon.com/bedrock/latest/userguide/conversation-inference-supported-models-features.html), watch out for Tool use)
144+
* **Automata** Full agent runtime. Orchestrates long-term multi-step interactions composable with `Prompter` and `Manifold` as subroutines. Supports planning, tool invocation, durable memory, and reflective reasoning. Encodes `A`, returns `B`.
145+
146+
This architecture allows you to start simple (one-shot prompts) and scale up to powerful autonomous systems with reasoning and memory - all within a consistent agent model.
147+
148+
149+
| Abstraction | Input Type | Output Type | Tool Use | Memory Scope | Reasoning | Composable | Purpose |
150+
| ----------- | ---------- | ----------- | -------- | ---------------------- | ------------------------- | -------------------- | ----------------------------------------------- |
151+
| `Prompter` | `A` | `Reply` || ❌ stateless || ✅ pipe | Simple LLM prompt-response, no side effects |
152+
| `Manifold` | `A` | `B` || ✅ ephemeral per call | ⚠️ LLM reasoning only | ✅ pipe | Tool-enhanced structured interaction |
153+
| `Automata` | `A` | `B` || ✅ durable / reflective | ✅ planning / goal setting | ✅ pipe / aggregation | Full agent loop with memory and decision-making |
154+
155+
156+
### Prompter
157+
158+
The diagram below emphasis core building blocks for `Prompter`.
159+
160+
```mermaid
161+
%%{init: {'theme':'neutral'}}%%
162+
graph TD
163+
subgraph Interface
164+
A[Type A]
165+
B[Type B]
166+
end
167+
subgraph Agent
168+
A --"01|input"--> E[Encoder]
169+
E --"02|prompt"--> G((Agent))
170+
G --"03|eval"--> L[LLM]
171+
G --"04|reply"--> B
172+
end
173+
```
174+
175+
### Manifold
176+
177+
The diagram below emphasis core building blocks for `Manifold`.
178+
179+
```mermaid
180+
%%{init: {'theme':'neutral'}}%%
181+
graph TD
182+
subgraph Interface
183+
A[Type A]
184+
B[Type B]
185+
end
186+
subgraph Commands
187+
C[Command]
188+
end
189+
subgraph Agent
190+
A --"01|input"--> E[Encoder]
191+
E --"02|prompt"--> G((Agent))
192+
G --"03|eval"--> L[LLM]
193+
G -."04|exec".-> C
194+
C -."05|result".-> G
195+
G --"06|reply"--> D[Decoder]
196+
D --"07|answer" --> B
197+
end
198+
```
199+
200+
### Automata
201+
202+
The diagram below emphasis core building blocks for `Automata`.
132203

133204
```mermaid
134205
%%{init: {'theme':'neutral'}}%%
@@ -158,6 +229,7 @@ graph TD
158229
end
159230
```
160231

232+
161233
Following this architecture, the agent is assembled from building blocks as lego constructor:
162234

163235
```go
@@ -213,8 +285,6 @@ const (
213285

214286
The following [reasoner classes](https://pkg.go.dev/github.com/kshard/thinker/reasoner) are supported:
215287
* *Void* always sets a new goal to return results.
216-
* *Cmd* sets the goal for agent to execute a single command and return the result if/when successful.
217-
* *CmdSeq* sets the goal for reasoner to execute sequence of commands.
218288
* *From* is fundamental constuctor for application specific reasoners.
219289
* *Epoch* is pseudo reasoner, it limits number of itterations agent takes to solve a task.
220290

agent.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,5 +43,5 @@ type State[B any] struct {
4343
Confidence float64
4444

4545
// Feedback to LLM
46-
Feedback chatter.Section
46+
Feedback chatter.Content
4747
}

agent/automata.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,11 @@ func (automata *Automata[A, B]) Prompt(ctx context.Context, input A, opt ...chat
5858
var nul B
5959
state := thinker.State[B]{Phase: thinker.AGENT_ASK, Epoch: 0}
6060

61+
switch v := automata.llm.(type) {
62+
case interface{ ResetQuota() }:
63+
v.ResetQuota()
64+
}
65+
6166
prompt, err := automata.encoder.Encode(input)
6267
if err != nil {
6368
return nul, err

agent/manifold.go

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
//
2+
// Copyright (C) 2025 Dmitry Kolesnikov
3+
//
4+
// This file may be modified and distributed under the terms
5+
// of the MIT license. See the LICENSE file for details.
6+
// https://github.com/kshard/thinker
7+
//
8+
9+
package agent
10+
11+
import (
12+
"context"
13+
"errors"
14+
15+
"github.com/kshard/chatter"
16+
"github.com/kshard/thinker"
17+
)
18+
19+
type Manifold[A, B any] struct {
20+
llm chatter.Chatter
21+
encoder thinker.Encoder[A]
22+
decoder thinker.Decoder[B]
23+
registry thinker.Registry
24+
}
25+
26+
func NewManifold[A, B any](
27+
llm chatter.Chatter,
28+
encoder thinker.Encoder[A],
29+
decoder thinker.Decoder[B],
30+
registry thinker.Registry,
31+
) *Manifold[A, B] {
32+
return &Manifold[A, B]{
33+
llm: llm,
34+
encoder: encoder,
35+
decoder: decoder,
36+
registry: registry,
37+
}
38+
}
39+
40+
func (manifold *Manifold[A, B]) Prompt(ctx context.Context, input A, opt ...chatter.Opt) (B, error) {
41+
var nul B
42+
43+
switch v := manifold.llm.(type) {
44+
case interface{ ResetQuota() }:
45+
v.ResetQuota()
46+
}
47+
48+
prompt, err := manifold.encoder.Encode(input)
49+
if err != nil {
50+
return nul, thinker.ErrCodec.With(err)
51+
}
52+
53+
opt = append(opt, manifold.registry.Context())
54+
memory := []chatter.Message{prompt}
55+
56+
for {
57+
reply, err := manifold.llm.Prompt(ctx, memory, opt...)
58+
if err != nil {
59+
return nul, thinker.ErrLLM.With(err)
60+
}
61+
62+
switch reply.Stage {
63+
case chatter.LLM_RETURN:
64+
_, ret, err := manifold.decoder.Decode(reply)
65+
if err != nil {
66+
var feedback chatter.Content
67+
if ok := errors.As(err, &feedback); !ok {
68+
return nul, err
69+
}
70+
71+
var prompt chatter.Prompt
72+
prompt.With(feedback)
73+
memory = append(memory, reply, &prompt)
74+
continue
75+
}
76+
return ret, nil
77+
case chatter.LLM_INCOMPLETE:
78+
_, ret, err := manifold.decoder.Decode(reply)
79+
if err != nil {
80+
var feedback chatter.Content
81+
if ok := errors.As(err, &feedback); !ok {
82+
return nul, err
83+
}
84+
85+
var prompt chatter.Prompt
86+
prompt.With(feedback)
87+
memory = append(memory, reply, &prompt)
88+
continue
89+
}
90+
return ret, nil
91+
case chatter.LLM_INVOKE:
92+
stage, answer, err := manifold.registry.Invoke(reply)
93+
if err != nil {
94+
return nul, thinker.ErrCmd.With(err)
95+
}
96+
switch stage {
97+
case thinker.AGENT_RETURN:
98+
_, ret, err := manifold.decoder.Decode(&chatter.Reply{Content: []chatter.Content{answer}})
99+
if err != nil {
100+
return nul, thinker.ErrCmd.With(err)
101+
}
102+
return ret, nil
103+
case thinker.AGENT_ABORT:
104+
return nul, thinker.ErrAborted
105+
default:
106+
memory = append(memory, reply, answer)
107+
}
108+
default:
109+
return nul, thinker.ErrAborted
110+
}
111+
}
112+
}

agent/prompter.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,10 @@ import (
1717

1818
// Prompter is memoryless and stateless agent, implementing request/response to LLMs.
1919
type Prompter[A any] struct {
20-
*Automata[A, string]
20+
*Automata[A, *chatter.Reply]
2121
}
2222

23-
func NewPrompter[A any](llm chatter.Chatter, f func(A) (*chatter.Prompt, error)) *Prompter[A] {
23+
func NewPrompter[A any](llm chatter.Chatter, f func(A) (chatter.Message, error)) *Prompter[A] {
2424
w := &Prompter[A]{}
2525
w.Automata = NewAutomata(
2626
llm,
@@ -41,7 +41,7 @@ func NewPrompter[A any](llm chatter.Chatter, f func(A) (*chatter.Prompt, error))
4141
// Configures the reasoner, which determines the agent's next actions and prompts.
4242
// Here, we use a void reasoner, meaning no reasoning is performed—the agent
4343
// simply returns the result.
44-
reasoner.NewVoid[string](),
44+
reasoner.NewVoid[*chatter.Reply](),
4545
)
4646

4747
return w

agent/jsonify.go renamed to agent/worker/jsonify.go

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,12 @@
66
// https://github.com/kshard/thinker
77
//
88

9-
package agent
9+
package worker
1010

1111
import (
1212
"github.com/kshard/chatter"
1313
"github.com/kshard/thinker"
14+
"github.com/kshard/thinker/agent"
1415
"github.com/kshard/thinker/codec"
1516
"github.com/kshard/thinker/memory"
1617
"github.com/kshard/thinker/prompt/jsonify"
@@ -19,7 +20,7 @@ import (
1920

2021
// Jsonify implementing request/response to LLMs, forcing the response to be JSON array.
2122
type Jsonify[A any] struct {
22-
*Automata[A, []string]
23+
*agent.Automata[A, []string]
2324
encoder thinker.Encoder[A]
2425
validator func([]string) error
2526
}
@@ -31,7 +32,7 @@ func NewJsonify[A any](
3132
validator func([]string) error,
3233
) *Jsonify[A] {
3334
w := &Jsonify[A]{encoder: encoder, validator: validator}
34-
w.Automata = NewAutomata(llm,
35+
w.Automata = agent.NewAutomata(llm,
3536

3637
// Configures memory for the agent. Typically, memory retains all of
3738
// the agent's observations. Here, we use an infinite stream memory,
@@ -57,13 +58,18 @@ func NewJsonify[A any](
5758
return w
5859
}
5960

60-
func (w *Jsonify[A]) encode(in A) (prompt *chatter.Prompt, err error) {
61-
prompt, err = w.encoder.Encode(in)
62-
if err == nil {
63-
jsonify.Strings.Harden(prompt)
61+
func (w *Jsonify[A]) encode(in A) (chatter.Message, error) {
62+
prompt, err := w.encoder.Encode(in)
63+
if err != nil {
64+
return nil, err
6465
}
6566

66-
return
67+
switch v := prompt.(type) {
68+
case *chatter.Prompt:
69+
jsonify.Strings.Harden(v)
70+
}
71+
72+
return prompt, nil
6773
}
6874

6975
func (w *Jsonify[A]) decode(reply *chatter.Reply) (float64, []string, error) {
@@ -79,7 +85,7 @@ func (w *Jsonify[A]) decode(reply *chatter.Reply) (float64, []string, error) {
7985
return 1.0, seq, nil
8086
}
8187

82-
func (w *Jsonify[A]) deduct(state thinker.State[[]string]) (thinker.Phase, *chatter.Prompt, error) {
88+
func (w *Jsonify[A]) deduct(state thinker.State[[]string]) (thinker.Phase, chatter.Message, error) {
8389
// Provide feedback to LLM if there are no confidence about the results
8490
if state.Feedback != nil && state.Confidence < 1.0 {
8591
var prompt chatter.Prompt

0 commit comments

Comments
 (0)