Skip to content

Commit 899ad81

Browse files
committed
v1.0.3: Add rich Event struct, Mock provider, OpenAI Responses provider
1 parent 2d86111 commit 899ad81

4 files changed

Lines changed: 399 additions & 8 deletions

File tree

agent.go

Lines changed: 33 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,14 @@ type Tool struct {
2323

2424
// Event represents a step in the agent's reasoning.
2525
type Event struct {
26-
Type string
27-
Content string
26+
Type string
27+
Content string
28+
ToolCallID string
29+
ToolName string
30+
Args map[string]interface{}
31+
Result string
32+
IsError bool
33+
Thinking string
2834
}
2935

3036
// Agent is the core reasoning loop.
@@ -75,14 +81,15 @@ func (a *Agent) Run(ctx context.Context, systemPrompt, userMessage string) (stri
7581
const maxIterations = 20
7682
for i := 0; i < maxIterations; i++ {
7783
a.logger.Info("agent iteration", "step", i+1)
84+
a.emit(Event{Type: string(EventTurnStart), Content: fmt.Sprintf("turn %d", i+1)})
7885

7986
response, err := a.provider.Complete(ctx, messages)
8087
if err != nil {
81-
a.emit(Event{Type: "error", Content: err.Error()})
88+
a.emit(Event{Type: string(EventError), Content: err.Error(), IsError: true})
8289
return "", fmt.Errorf("provider error at step %d: %w", i+1, err)
8390
}
8491

85-
a.emit(Event{Type: "thought", Content: response})
92+
a.emit(Event{Type: string(EventMessageUpdate), Content: response})
8693

8794
messages = append(messages, Message{
8895
Role: "assistant",
@@ -91,7 +98,8 @@ func (a *Agent) Run(ctx context.Context, systemPrompt, userMessage string) (stri
9198

9299
calls := ParseToolCalls(response)
93100
if len(calls) == 0 {
94-
a.emit(Event{Type: "done", Content: response})
101+
a.emit(Event{Type: string(EventMessageEnd), Content: response})
102+
a.emit(Event{Type: string(EventTurnEnd), Content: ""})
95103
return response, nil
96104
}
97105

@@ -101,26 +109,43 @@ func (a *Agent) Run(ctx context.Context, systemPrompt, userMessage string) (stri
101109
if !ok {
102110
result := fmt.Sprintf("unknown tool: %s", call.Tool)
103111
toolResults.WriteString(fmt.Sprintf("Tool %s: %s\n", call.Tool, result))
104-
a.emit(Event{Type: "tool_result", Content: result})
112+
a.emit(Event{Type: string(EventToolExecutionEnd), ToolName: call.Tool, Result: result, IsError: true})
105113
continue
106114
}
107115

108-
a.emit(Event{Type: "tool_call", Content: fmt.Sprintf("%s(%v)", call.Tool, call.Args)})
116+
argsMap := make(map[string]interface{})
117+
for k, v := range call.Args {
118+
argsMap[k] = v
119+
}
120+
a.emit(Event{
121+
Type: string(EventToolExecutionStart),
122+
ToolName: call.Tool,
123+
ToolCallID: call.Tool + "-" + fmt.Sprintf("%d", i),
124+
Args: argsMap,
125+
})
109126
a.logger.Info("executing tool", "tool", call.Tool, "args", call.Args)
110127

111128
result, err := tool.Execute(ctx, call.Args)
129+
isError := false
112130
if err != nil {
113131
result = fmt.Sprintf("ERROR: %s\nOutput: %s", err.Error(), result)
132+
isError = true
114133
}
115134

116-
a.emit(Event{Type: "tool_result", Content: result})
135+
a.emit(Event{
136+
Type: string(EventToolExecutionEnd),
137+
ToolName: call.Tool,
138+
Result: result,
139+
IsError: isError,
140+
})
117141
toolResults.WriteString(fmt.Sprintf("Tool %s result:\n%s\n\n", call.Tool, result))
118142
}
119143

120144
messages = append(messages, Message{
121145
Role: "user",
122146
Content: toolResults.String(),
123147
})
148+
a.emit(Event{Type: string(EventTurnEnd), Content: ""})
124149
}
125150

126151
return "", fmt.Errorf("agent exceeded max iterations (%d)", maxIterations)

mock.go

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
package iteragent
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"strings"
7+
)
8+
9+
type MockProvider struct {
10+
model string
11+
response string
12+
toolCalls []ToolCall
13+
toolCallIndex int
14+
error error
15+
}
16+
17+
func NewMock(response string) *MockProvider {
18+
return &MockProvider{
19+
model: "mock",
20+
response: response,
21+
}
22+
}
23+
24+
func NewMockWithTools(response string, toolCalls []ToolCall) *MockProvider {
25+
return &MockProvider{
26+
model: "mock",
27+
response: response,
28+
toolCalls: toolCalls,
29+
}
30+
}
31+
32+
func NewMockWithError(err error) *MockProvider {
33+
return &MockProvider{
34+
model: "mock",
35+
error: err,
36+
}
37+
}
38+
39+
func (p *MockProvider) Name() string {
40+
return fmt.Sprintf("mock(%s)", p.model)
41+
}
42+
43+
func (p *MockProvider) Complete(ctx context.Context, messages []Message) (string, error) {
44+
if p.error != nil {
45+
return "", p.error
46+
}
47+
48+
if p.toolCallIndex < len(p.toolCalls) {
49+
call := p.toolCalls[p.toolCallIndex]
50+
p.toolCallIndex++
51+
return fmt.Sprintf("```tool\n%s\n```", mustJson(call)), nil
52+
}
53+
54+
return p.response, nil
55+
}
56+
57+
func (p *MockProvider) Stream(ctx context.Context, config StreamConfig, messages []Message, onEvent func(StreamEvent)) (Message, error) {
58+
if p.error != nil {
59+
return Message{}, p.error
60+
}
61+
62+
for _, char := range strings.Split(p.response, "") {
63+
select {
64+
case <-ctx.Done():
65+
return Message{}, ctx.Err()
66+
default:
67+
}
68+
onEvent(StreamEvent{
69+
Type: StreamEventContent,
70+
Content: char,
71+
})
72+
}
73+
74+
return Message{
75+
Role: "assistant",
76+
Content: p.response,
77+
}, nil
78+
}
79+
80+
func mustJson(v interface{}) string {
81+
switch v := v.(type) {
82+
case string:
83+
return v
84+
default:
85+
return fmt.Sprintf("%+v", v)
86+
}
87+
}
88+
89+
type MockProviderBuilder struct {
90+
mock *MockProvider
91+
}
92+
93+
func Mock() *MockProviderBuilder {
94+
return &MockProviderBuilder{
95+
mock: &MockProvider{
96+
model: "mock",
97+
},
98+
}
99+
}
100+
101+
func (b *MockProviderBuilder) Text(text string) *MockProviderBuilder {
102+
b.mock.response = text
103+
return b
104+
}
105+
106+
func (b *MockProviderBuilder) Model(model string) *MockProviderBuilder {
107+
b.mock.model = model
108+
return b
109+
}
110+
111+
func (b *MockProviderBuilder) WithTools(toolCalls ...ToolCall) *MockProviderBuilder {
112+
b.mock.toolCalls = toolCalls
113+
return b
114+
}
115+
116+
func (b *MockProviderBuilder) WithError(err error) *MockProviderBuilder {
117+
b.mock.error = err
118+
return b
119+
}
120+
121+
func (b *MockProviderBuilder) Build() Provider {
122+
return b.mock
123+
}
124+
125+
type MockStreamProvider struct {
126+
events []StreamEvent
127+
index int
128+
}
129+
130+
func NewMockStreamProvider(events []StreamEvent) *MockStreamProvider {
131+
return &MockStreamProvider{
132+
events: events,
133+
}
134+
}
135+
136+
func (p *MockStreamProvider) Stream(ctx context.Context, config StreamConfig, messages []Message, onEvent func(StreamEvent)) (Message, error) {
137+
var content strings.Builder
138+
139+
for _, event := range p.events {
140+
select {
141+
case <-ctx.Done():
142+
return Message{}, ctx.Err()
143+
default:
144+
}
145+
onEvent(event)
146+
if event.Content != "" {
147+
content.WriteString(event.Content)
148+
}
149+
}
150+
151+
return Message{
152+
Role: "assistant",
153+
Content: content.String(),
154+
}, nil
155+
}

0 commit comments

Comments
 (0)