Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ jobs:

- name: go test
run: |
mkdir -p /tmp/softcmd
mkdir -p /tmp/cmd
go test -v -coverprofile=profile.cov $(go list ./... | grep -v /examples/)

- uses: shogo82148/actions-goveralls@v1
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/check-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ jobs:

- name: go test
run: |
mkdir -p /tmp/softcmd
mkdir -p /tmp/cmd
go test -v -coverprofile=profile.cov $(go list ./... | grep -v /examples/)

- uses: shogo82148/actions-goveralls@v1
Expand Down
82 changes: 76 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,23 @@ The generative agents autonomously generate output as a reaction on input, past

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.

## Design Philosophy

1. **Minimal**: No hidden magic; each component is explicit.
2. **Typed**: Inputs and outputs are type-safe (A, B).
3. **Composable**: Agents can be used standalone, orchestrated by higher layers or piped.
4. **Interoperable**: Supports integration with LLMs (e.g. Bedrock, OpenAI) and tools.

## Getting started

- [Inspiration](#inspiration)
- [Design Philosophy](#design-philosophy)
- [Getting started](#getting-started)
- [Quick example](#quick-example)
- [Agent Architecture](#agent-architecture)
- [Prompter](#prompter)
- [Manifold](#manifold)
- [Automata](#automata)
- [Memory](#memory)
- [Reasoner](#reasoner)
- [Encoder \& Decoder](#encoder--decoder)
Expand All @@ -60,9 +73,6 @@ In this library, an agent is defined as a side-effect function `ƒ: A ⟼ B`, wh
- [bugs](#bugs)
- [License](#license)


## Getting started

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.

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

## Agent Architecture

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.
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:
* **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.
* **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)
* **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`.

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.


| Abstraction | Input Type | Output Type | Tool Use | Memory Scope | Reasoning | Composable | Purpose |
| ----------- | ---------- | ----------- | -------- | ---------------------- | ------------------------- | -------------------- | ----------------------------------------------- |
| `Prompter` | `A` | `Reply` | ❌ | ❌ stateless | ❌ | ✅ pipe | Simple LLM prompt-response, no side effects |
| `Manifold` | `A` | `B` | ✅ | ✅ ephemeral per call | ⚠️ LLM reasoning only | ✅ pipe | Tool-enhanced structured interaction |
| `Automata` | `A` | `B` | ✅ | ✅ durable / reflective | ✅ planning / goal setting | ✅ pipe / aggregation | Full agent loop with memory and decision-making |


### Prompter

The diagram below emphasis core building blocks for `Prompter`.

```mermaid
%%{init: {'theme':'neutral'}}%%
graph TD
subgraph Interface
A[Type A]
B[Type B]
end
subgraph Agent
A --"01|input"--> E[Encoder]
E --"02|prompt"--> G((Agent))
G --"03|eval"--> L[LLM]
G --"04|reply"--> B
end
```

### Manifold

The diagram below emphasis core building blocks for `Manifold`.

```mermaid
%%{init: {'theme':'neutral'}}%%
graph TD
subgraph Interface
A[Type A]
B[Type B]
end
subgraph Commands
C[Command]
end
subgraph Agent
A --"01|input"--> E[Encoder]
E --"02|prompt"--> G((Agent))
G --"03|eval"--> L[LLM]
G -."04|exec".-> C
C -."05|result".-> G
G --"06|reply"--> D[Decoder]
D --"07|answer" --> B
end
```

### Automata

The diagram below emphasis core building blocks for `Automata`.

```mermaid
%%{init: {'theme':'neutral'}}%%
Expand Down Expand Up @@ -158,6 +229,7 @@ graph TD
end
```


Following this architecture, the agent is assembled from building blocks as lego constructor:

```go
Expand Down Expand Up @@ -213,8 +285,6 @@ const (

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

Expand Down
2 changes: 1 addition & 1 deletion agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,5 +43,5 @@ type State[B any] struct {
Confidence float64

// Feedback to LLM
Feedback chatter.Section
Feedback chatter.Content
}
5 changes: 5 additions & 0 deletions agent/automata.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,11 @@ func (automata *Automata[A, B]) Prompt(ctx context.Context, input A, opt ...chat
var nul B
state := thinker.State[B]{Phase: thinker.AGENT_ASK, Epoch: 0}

switch v := automata.llm.(type) {
case interface{ ResetQuota() }:
v.ResetQuota()
}

prompt, err := automata.encoder.Encode(input)
if err != nil {
return nul, err
Expand Down
112 changes: 112 additions & 0 deletions agent/manifold.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
//
// Copyright (C) 2025 Dmitry Kolesnikov
//
// This file may be modified and distributed under the terms
// of the MIT license. See the LICENSE file for details.
// https://github.com/kshard/thinker
//

package agent

import (
"context"
"errors"

"github.com/kshard/chatter"
"github.com/kshard/thinker"
)

type Manifold[A, B any] struct {
llm chatter.Chatter
encoder thinker.Encoder[A]
decoder thinker.Decoder[B]
registry thinker.Registry
}

func NewManifold[A, B any](
llm chatter.Chatter,
encoder thinker.Encoder[A],
decoder thinker.Decoder[B],
registry thinker.Registry,
) *Manifold[A, B] {
return &Manifold[A, B]{
llm: llm,
encoder: encoder,
decoder: decoder,
registry: registry,
}
}

func (manifold *Manifold[A, B]) Prompt(ctx context.Context, input A, opt ...chatter.Opt) (B, error) {
var nul B

switch v := manifold.llm.(type) {
case interface{ ResetQuota() }:
v.ResetQuota()
}

prompt, err := manifold.encoder.Encode(input)
if err != nil {
return nul, thinker.ErrCodec.With(err)
}

opt = append(opt, manifold.registry.Context())
memory := []chatter.Message{prompt}

for {
reply, err := manifold.llm.Prompt(ctx, memory, opt...)
if err != nil {
return nul, thinker.ErrLLM.With(err)
}

switch reply.Stage {
case chatter.LLM_RETURN:
_, ret, err := manifold.decoder.Decode(reply)
if err != nil {
var feedback chatter.Content
if ok := errors.As(err, &feedback); !ok {
return nul, err
}

var prompt chatter.Prompt
prompt.With(feedback)
memory = append(memory, reply, &prompt)
continue
}
return ret, nil
case chatter.LLM_INCOMPLETE:
_, ret, err := manifold.decoder.Decode(reply)
if err != nil {
var feedback chatter.Content
if ok := errors.As(err, &feedback); !ok {
return nul, err
}

var prompt chatter.Prompt
prompt.With(feedback)
memory = append(memory, reply, &prompt)
continue
}
return ret, nil
case chatter.LLM_INVOKE:
stage, answer, err := manifold.registry.Invoke(reply)
if err != nil {
return nul, thinker.ErrCmd.With(err)
}
switch stage {
case thinker.AGENT_RETURN:
_, ret, err := manifold.decoder.Decode(&chatter.Reply{Content: []chatter.Content{answer}})
if err != nil {
return nul, thinker.ErrCmd.With(err)
}
return ret, nil
case thinker.AGENT_ABORT:
return nul, thinker.ErrAborted
default:
memory = append(memory, reply, answer)
}
default:
return nul, thinker.ErrAborted
}
}
}
6 changes: 3 additions & 3 deletions agent/prompter.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,10 @@ import (

// Prompter is memoryless and stateless agent, implementing request/response to LLMs.
type Prompter[A any] struct {
*Automata[A, string]
*Automata[A, *chatter.Reply]
}

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

return w
Expand Down
24 changes: 15 additions & 9 deletions agent/jsonify.go → agent/worker/jsonify.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,12 @@
// https://github.com/kshard/thinker
//

package agent
package worker

import (
"github.com/kshard/chatter"
"github.com/kshard/thinker"
"github.com/kshard/thinker/agent"
"github.com/kshard/thinker/codec"
"github.com/kshard/thinker/memory"
"github.com/kshard/thinker/prompt/jsonify"
Expand All @@ -19,7 +20,7 @@ import (

// Jsonify implementing request/response to LLMs, forcing the response to be JSON array.
type Jsonify[A any] struct {
*Automata[A, []string]
*agent.Automata[A, []string]
encoder thinker.Encoder[A]
validator func([]string) error
}
Expand All @@ -31,7 +32,7 @@ func NewJsonify[A any](
validator func([]string) error,
) *Jsonify[A] {
w := &Jsonify[A]{encoder: encoder, validator: validator}
w.Automata = NewAutomata(llm,
w.Automata = agent.NewAutomata(llm,

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

func (w *Jsonify[A]) encode(in A) (prompt *chatter.Prompt, err error) {
prompt, err = w.encoder.Encode(in)
if err == nil {
jsonify.Strings.Harden(prompt)
func (w *Jsonify[A]) encode(in A) (chatter.Message, error) {
prompt, err := w.encoder.Encode(in)
if err != nil {
return nil, err
}

return
switch v := prompt.(type) {
case *chatter.Prompt:
jsonify.Strings.Harden(v)
}

return prompt, nil
}

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

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