Skip to content

Commit 687c2e3

Browse files
authored
Use a struct with receiver methods to encapsulate state (#6)
1 parent ff166a3 commit 687c2e3

File tree

14 files changed

+85
-51
lines changed

14 files changed

+85
-51
lines changed

README.md

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ In this library, an agent is defined as a side-effect function `ƒ: A ⟼ B`, wh
5151
- [Commands \& Tools](#commands--tools)
5252
- [Supported commands](#supported-commands)
5353
- [Chaining agents](#chaining-agents)
54+
- [FAQ](#faq)
5455
- [How To Contribute](#how-to-contribute)
5556
- [commit message](#commit-message)
5657
- [bugs](#bugs)
@@ -205,6 +206,40 @@ The `thinker` library does not provide built-in mechanisms for chaining agents.
205206
The [chain example](./examples/chain/chain.go) demostrates off-the-shelf techniques for agents chaining.
206207

207208

209+
## FAQ
210+
211+
<details>
212+
<summary>Do agents support concurrent execution?</summary>
213+
214+
This design does not support concurency on the purpose - the pure actor architecture is used. The agent follows a sequential decision-making loop:
215+
* Inner for {} loop causes each step depends on the previous result to maintain conversational causal effect
216+
* While memory is thread-safe and sharable among agents in the pipeline. It is not design to support multiple isolated session.
217+
* LLM calls are synchronous.
218+
219+
To enable concurrency, the application have to implement worker pools.
220+
</details>
221+
222+
223+
<details>
224+
<summary>How can an agent maintain a global state accessible to the encoder, decoder, and reasoner?</summary>
225+
226+
Use a struct with receiver methods to encapsulate state and provide direct access to the encoder, decoder, and reasoner. This keeps state management simple and idiomatic in Go.
227+
228+
```go
229+
type Agent struct{
230+
// declare global state
231+
}
232+
233+
func (*Agent) Encode(string) (prompt chatter.Prompt, err error) { /* ... */ }
234+
235+
func (*Agent) Decode(chatter.Reply) (float64, string, error) { /* ... */ }
236+
237+
func (*Agent) Deduct(thinker.State[string]) (thinker.Phase, chatter.Prompt, error) { /* ... */ }
238+
```
239+
</details>
240+
241+
242+
208243
## How To Contribute
209244

210245
The library is [MIT](LICENSE) licensed and accepts contributions via GitHub pull requests:

agent.go

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,16 +29,13 @@ const (
2929
)
3030

3131
// State of the agent, maintained by the agent and used by Reasoner.
32-
type State[A, B any] struct {
32+
type State[B any] struct {
3333
// Execution phase of the agent
3434
Phase Phase
3535

3636
// Current epoch of execution phase
3737
Epoch int
3838

39-
// Input to LLM
40-
Input A
41-
4239
// Reply from LLM
4340
Reply B
4441

agent/automata.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,15 @@ import (
1919
type Automata[A, B any] struct {
2020
llm chatter.Chatter
2121
memory thinker.Memory
22-
reasoner thinker.Reasoner[A, B]
22+
reasoner thinker.Reasoner[B]
2323
encoder thinker.Encoder[A]
2424
decoder thinker.Decoder[B]
2525
}
2626

2727
func NewAutomata[A, B any](
2828
llm chatter.Chatter,
2929
memory thinker.Memory,
30-
reasoner thinker.Reasoner[A, B],
30+
reasoner thinker.Reasoner[B],
3131
encoder thinker.Encoder[A],
3232
decoder thinker.Decoder[B],
3333
) *Automata[A, B] {
@@ -42,7 +42,7 @@ func NewAutomata[A, B any](
4242

4343
func (automata *Automata[A, B]) Prompt(ctx context.Context, input A, opt ...chatter.Opt) (B, error) {
4444
var nul B
45-
state := thinker.State[A, B]{Phase: thinker.AGENT_ASK, Epoch: 0, Input: input}
45+
state := thinker.State[B]{Phase: thinker.AGENT_ASK, Epoch: 0}
4646

4747
prompt, err := automata.encoder.Encode(input)
4848
if err != nil {
@@ -75,7 +75,7 @@ func (automata *Automata[A, B]) Prompt(ctx context.Context, input A, opt ...chat
7575

7676
switch phase {
7777
case thinker.AGENT_ASK:
78-
state = thinker.State[A, B]{Phase: thinker.AGENT_ASK, Epoch: 0}
78+
state = thinker.State[B]{Phase: thinker.AGENT_ASK, Epoch: 0}
7979
prompt = request
8080
shortMemory = automata.memory.Context(prompt)
8181
continue

examples/chain/chain.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ func NewAgentA(llm chatter.Chatter) *AgentA {
3636
agt := &AgentA{}
3737
agt.Automata = agent.NewAutomata(llm,
3838
memory.NewVoid(),
39-
reasoner.NewVoid[string, string](),
39+
reasoner.NewVoid[string](),
4040
codec.FromEncoder(agt.story),
4141
codec.DecoderID,
4242
)
@@ -69,15 +69,15 @@ func NewAgentB(llm chatter.Chatter) *AgentB {
6969
You are automomous agent who uses tools to perform required tasks.
7070
You are using and remember context from earlier chat history to execute the task.
7171
`),
72-
reasoner.NewEpoch(4, reasoner.From(agt.deduct)),
73-
codec.FromEncoder(agt.encode),
72+
reasoner.NewEpoch(4, agt),
73+
agt,
7474
agt.registry,
7575
)
7676

7777
return agt
7878
}
7979

80-
func (agt AgentB) encode(string) (prompt chatter.Prompt, err error) {
80+
func (agt AgentB) Encode(string) (prompt chatter.Prompt, err error) {
8181
prompt.WithTask(`
8282
Use available tools to complete the workflow:
8383
(1) Use available tools to read files one by one.
@@ -89,7 +89,7 @@ func (agt AgentB) encode(string) (prompt chatter.Prompt, err error) {
8989
return
9090
}
9191

92-
func (AgentB) deduct(state thinker.State[string, thinker.CmdOut]) (thinker.Phase, chatter.Prompt, error) {
92+
func (AgentB) Deduct(state thinker.State[thinker.CmdOut]) (thinker.Phase, chatter.Prompt, error) {
9393
// the registry has failed to execute command, we have to supply the feedback to LLM
9494
if state.Feedback != nil && state.Confidence < 1.0 {
9595
var prompt chatter.Prompt

examples/helloworld/hw.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ func main() {
6363
// Configures the reasoner, which determines the agent's next actions and prompts.
6464
// Here, we use a void reasoner, meaning no reasoning is performed—the agent
6565
// simply returns the result.
66-
reasoner.NewVoid[string, string](),
66+
reasoner.NewVoid[string](),
6767

6868
// Configures the encoder to transform input of type A into a `chatter.Prompt`.
6969
// Here, we use an encoder that converts string expressions into prompt.

examples/rainbow/rainbow.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ func validate(seq []string) error {
6767
}
6868

6969
// deduct new goal for the agent to pursue.
70-
func deduct(state thinker.State[any, []string]) (thinker.Phase, chatter.Prompt, error) {
70+
func deduct(state thinker.State[[]string]) (thinker.Phase, chatter.Prompt, error) {
7171
// Provide feedback to LLM if there are no confidence about the results
7272
if state.Feedback != nil && state.Confidence < 1.0 {
7373
var prompt chatter.Prompt

examples/script/script.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ func encode(q string) (prompt chatter.Prompt, err error) {
4242

4343
// deduct new goal for the agent to pursue.
4444
// Note, the agent uses registry as decoder therefore agent is string -> thinker.CmdOut
45-
func deduct(state thinker.State[string, thinker.CmdOut]) (thinker.Phase, chatter.Prompt, error) {
45+
func deduct(state thinker.State[thinker.CmdOut]) (thinker.Phase, chatter.Prompt, error) {
4646
// the registry has failed to execute command, we have to supply the feedback to LLM
4747
if state.Feedback != nil && state.Confidence < 1.0 {
4848
var prompt chatter.Prompt

reasoner.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import (
1717
// or non-deterministic analysis of immediate results and past experiences.
1818
// Based on this assessment, it determines whether the goal has been achieved
1919
// and, if not, suggests the best new goal for the agent to pursue.
20-
type Reasoner[A, B any] interface {
20+
type Reasoner[B any] interface {
2121
// Deduct new goal for the agent to pursue.
22-
Deduct(State[A, B]) (Phase, chatter.Prompt, error)
22+
Deduct(State[B]) (Phase, chatter.Prompt, error)
2323
}

reasoner/command.go

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,19 @@ import (
1616
"github.com/kshard/thinker/command"
1717
)
1818

19+
// State of Command reasoner
20+
type StateCmd = thinker.State[thinker.CmdOut]
21+
1922
// The cmd (command) reasoner set the goal for agent to execute a single command.
2023
// It returns right after the command return results.
21-
type Cmd[A any] struct{}
24+
type Cmd struct{}
2225

2326
// Creates new command reasoner.
24-
func NewCmd[A any]() *Cmd[A] {
25-
return &Cmd[A]{}
27+
func NewCmd() *Cmd {
28+
return &Cmd{}
2629
}
2730

28-
func (task *Cmd[A]) Deduct(state thinker.State[A, thinker.CmdOut]) (thinker.Phase, chatter.Prompt, error) {
31+
func (task *Cmd) Deduct(state StateCmd) (thinker.Phase, chatter.Prompt, error) {
2932
if state.Feedback != nil && state.Confidence < 1.0 {
3033
var prompt chatter.Prompt
3134
prompt.WithTask("Refine the previous operation using the feedback below.")
@@ -45,13 +48,13 @@ func (task *Cmd[A]) Deduct(state thinker.State[A, thinker.CmdOut]) (thinker.Phas
4548

4649
// The sequence of cmd (commands) reasoner set the goal for agent to execute a sequence of commands.
4750
// The reason returns only after LLM uses return command.
48-
type CmdSeq[A any] struct{}
51+
type CmdSeq struct{}
4952

50-
func NewCmdSeq[A any]() *CmdSeq[A] {
51-
return &CmdSeq[A]{}
53+
func NewCmdSeq() *CmdSeq {
54+
return &CmdSeq{}
5255
}
5356

54-
func (task *CmdSeq[A]) Deduct(state thinker.State[A, thinker.CmdOut]) (thinker.Phase, chatter.Prompt, error) {
57+
func (task *CmdSeq) Deduct(state StateCmd) (thinker.Phase, chatter.Prompt, error) {
5558
if state.Feedback != nil && state.Confidence < 1.0 {
5659
var prompt chatter.Prompt
5760
prompt.WithTask("Refine the previous workflow step using the feedback below.")

reasoner/command_test.go

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,10 @@ import (
1919
)
2020

2121
func TestCmdDeduct(t *testing.T) {
22-
r := reasoner.NewCmd[string]()
22+
r := reasoner.NewCmd()
2323

2424
t.Run("Refine", func(t *testing.T) {
25-
phase, prompt, err := r.Deduct(thinker.State[string, thinker.CmdOut]{
25+
phase, prompt, err := r.Deduct(reasoner.StateCmd{
2626
Feedback: chatter.Feedback("feedback"),
2727
Confidence: 0.1,
2828
})
@@ -35,7 +35,7 @@ func TestCmdDeduct(t *testing.T) {
3535
})
3636

3737
t.Run("Return", func(t *testing.T) {
38-
phase, _, err := r.Deduct(thinker.State[string, thinker.CmdOut]{
38+
phase, _, err := r.Deduct(reasoner.StateCmd{
3939
Reply: thinker.CmdOut{Cmd: command.BASH, Output: "Bash Output"},
4040
})
4141

@@ -46,7 +46,7 @@ func TestCmdDeduct(t *testing.T) {
4646
})
4747

4848
t.Run("Abort", func(t *testing.T) {
49-
phase, _, _ := r.Deduct(thinker.State[string, thinker.CmdOut]{})
49+
phase, _, _ := r.Deduct(reasoner.StateCmd{})
5050

5151
it.Then(t).Should(
5252
it.Equal(phase, thinker.AGENT_ABORT),
@@ -56,10 +56,10 @@ func TestCmdDeduct(t *testing.T) {
5656
}
5757

5858
func TestCmdSeqDeduct(t *testing.T) {
59-
r := reasoner.NewCmdSeq[string]()
59+
r := reasoner.NewCmdSeq()
6060

6161
t.Run("Refine", func(t *testing.T) {
62-
phase, prompt, err := r.Deduct(thinker.State[string, thinker.CmdOut]{
62+
phase, prompt, err := r.Deduct(reasoner.StateCmd{
6363
Feedback: chatter.Feedback("feedback"),
6464
Confidence: 0.1,
6565
})
@@ -72,7 +72,7 @@ func TestCmdSeqDeduct(t *testing.T) {
7272
})
7373

7474
t.Run("Return", func(t *testing.T) {
75-
phase, _, err := r.Deduct(thinker.State[string, thinker.CmdOut]{
75+
phase, _, err := r.Deduct(reasoner.StateCmd{
7676
Reply: thinker.CmdOut{Cmd: command.RETURN},
7777
})
7878

@@ -83,7 +83,7 @@ func TestCmdSeqDeduct(t *testing.T) {
8383
})
8484

8585
t.Run("Continue", func(t *testing.T) {
86-
phase, prompt, err := r.Deduct(thinker.State[string, thinker.CmdOut]{
86+
phase, prompt, err := r.Deduct(reasoner.StateCmd{
8787
Reply: thinker.CmdOut{Cmd: command.BASH, Output: "Bash Output"},
8888
})
8989

@@ -96,11 +96,10 @@ func TestCmdSeqDeduct(t *testing.T) {
9696
})
9797

9898
t.Run("Abort", func(t *testing.T) {
99-
phase, _, _ := r.Deduct(thinker.State[string, thinker.CmdOut]{})
99+
phase, _, _ := r.Deduct(reasoner.StateCmd{})
100100

101101
it.Then(t).Should(
102102
it.Equal(phase, thinker.AGENT_ABORT),
103103
)
104104
})
105-
106105
}

0 commit comments

Comments
 (0)