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
864 changes: 456 additions & 408 deletions backend/agent/conv.go

Large diffs are not rendered by default.

119 changes: 63 additions & 56 deletions backend/agent/task_reconciler.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,9 @@ import (
"github.com/furisto/construct/backend/model"
"github.com/furisto/construct/backend/prompt"
"github.com/furisto/construct/backend/skill"
"github.com/furisto/construct/backend/tool/base"
"github.com/furisto/construct/backend/tool/codeact"
tooltypes "github.com/furisto/construct/backend/tool/types"
"github.com/furisto/construct/shared"
"github.com/furisto/construct/shared/conv"
"github.com/google/uuid"
"github.com/prometheus/client_golang/prometheus"
"github.com/spf13/afero"
Expand Down Expand Up @@ -718,63 +717,79 @@ func (r *TaskReconciler) reconcileExecuteTools(ctx context.Context, taskID uuid.
return Result{Retry: true}, nil
}

func (r *TaskReconciler) callTools(ctx context.Context, task *memory.Task, message *memory.Message) ([]base.ToolResult, map[string]int64, error) {
func (r *TaskReconciler) callTools(ctx context.Context, task *memory.Task, message *memory.Message) ([]*ToolResult, map[string]int64, error) {
logger := r.logger.With(
KeyTaskID, task.ID,
KeyMessageID, message.ID,
)
LogOperationStart(logger, "call tools")

var toolResults []base.ToolResult
var toolResults []*ToolResult
toolStats := make(map[string]int64)

for _, block := range message.Content.Blocks {
switch block.Kind {
case types.MessageBlockKindCodeInterpreterCall:
var toolCall model.ToolCallBlock
case types.MessageBlockKindToolCall:
var toolCall ToolCall
err := json.Unmarshal([]byte(block.Payload), &toolCall)
if err != nil {
logger.ErrorContext(ctx, "failed to unmarshal tool call", "error", err)
return nil, nil, fmt.Errorf("failed to unmarshal tool call: %w", err)
}
logInterpreterArgs(ctx, task.ID, toolCall.ID, toolCall.Args)

toolStart := time.Now()
result, err := r.interpreter.Interpret(ctx, afero.NewOsFs(), toolCall.Args, &codeact.Task{
ID: task.ID,
ProjectDirectory: task.ProjectDirectory,
})
toolDuration := time.Since(toolStart)
if toolCall.Tool == "code_interpreter" && toolCall.Input != nil && toolCall.Input.Interpreter != nil {
// Log interpreter args
inputJSON, _ := json.Marshal(toolCall.Input.Interpreter)
logInterpreterArgs(ctx, task.ID, toolCall.Provider.ID, inputJSON)

if errors.Is(ctx.Err(), context.Canceled) {
err = errors.New("tool execution was cancelled by user. Wait for further instructions")
}
toolStart := time.Now()
result, err := r.interpreter.Interpret(ctx, afero.NewOsFs(), toolCall.Input.Interpreter, &codeact.Task{
ID: task.ID,
ProjectDirectory: task.ProjectDirectory,
})
toolDuration := time.Since(toolStart)

success := err == nil
if !success {
LogError(logger, "code interpreter execution failed", err, KeyToolDuration, toolDuration.Milliseconds())
} else {
logger.DebugContext(ctx, "code interpreter execution completed",
"duration_ms", toolDuration.Milliseconds(),
"success", true,
)
}
interpreterResult := &codeact.InterpreterToolResult{
ID: toolCall.ID,
Output: result.ConsoleOutput,
FunctionCalls: result.FunctionCalls,
Error: conv.ErrorToString(err),
}
toolResults = append(toolResults, interpreterResult)

for tool, count := range result.ToolStats {
toolStats[tool] += count
logger.DebugContext(ctx, "tool invoked",
KeyToolName, tool,
"count", count,
)
if errors.Is(ctx.Err(), context.Canceled) {
err = errors.New("tool execution was cancelled by user. Wait for further instructions")
}

success := err == nil
if !success {
LogError(logger, "code interpreter execution failed", err, KeyToolDuration, toolDuration.Milliseconds())
} else {
logger.DebugContext(ctx, "code interpreter execution completed",
"duration_ms", toolDuration.Milliseconds(),
"success", true,
)
}

// Create unified ToolResult directly
toolResult := &ToolResult{
Tool: toolCall.Tool,
Output: &tooltypes.ToolOutput{
Interpreter: &tooltypes.InterpreterOutput{
ConsoleOutput: result.ConsoleOutput,
FunctionCalls: result.FunctionCalls,
ToolStats: result.ToolStats,
},
},
Succeeded: err == nil,
Provider: &ProviderData{
Kind: toolCall.Provider.Kind,
ID: toolCall.Provider.ID,
},
}
toolResults = append(toolResults, toolResult)

for tool, count := range result.ToolStats {
toolStats[tool] += count
logger.DebugContext(ctx, "tool invoked",
KeyToolName, tool,
"count", count,
)
}
logInterpreterResult(ctx, task.ID, toolCall.Provider.ID, toolResult)
}
logInterpreterResult(ctx, task.ID, toolCall.ID, interpreterResult)
}
}

Expand All @@ -786,26 +801,18 @@ func (r *TaskReconciler) callTools(ctx context.Context, task *memory.Task, messa
return toolResults, toolStats, nil
}

func (r *TaskReconciler) persistToolResults(ctx context.Context, taskID uuid.UUID, toolResults []base.ToolResult, tx *memory.Client) (*memory.Message, error) {
func (r *TaskReconciler) persistToolResults(ctx context.Context, taskID uuid.UUID, toolResults []*ToolResult, tx *memory.Client) (*memory.Message, error) {
toolBlocks := make([]types.MessageBlock, 0, len(toolResults))
for _, result := range toolResults {
jsonResult, err := json.Marshal(result)
for _, toolResult := range toolResults {
jsonResult, err := json.Marshal(toolResult)
if err != nil {
return nil, fmt.Errorf("failed to marshal tool result: %w", err)
}

switch result.(type) {
case *codeact.InterpreterToolResult:
toolBlocks = append(toolBlocks, types.MessageBlock{
Kind: types.MessageBlockKindCodeInterpreterResult,
Payload: string(jsonResult),
})
default:
toolBlocks = append(toolBlocks, types.MessageBlock{
Kind: types.MessageBlockKindNativeToolResult,
Payload: string(jsonResult),
})
}
toolBlocks = append(toolBlocks, types.MessageBlock{
Kind: types.MessageBlockKindToolResult,
Payload: string(jsonResult),
})
}

return tx.Message.Create().
Expand Down Expand Up @@ -835,7 +842,7 @@ func logInterpreterArgs(ctx context.Context, taskID uuid.UUID, toolID string, ar
logInterpreter(ctx, taskID, toolID, a.Script, "args_interpreter")
}

func logInterpreterResult(ctx context.Context, taskID uuid.UUID, toolID string, result *codeact.InterpreterToolResult) {
func logInterpreterResult(ctx context.Context, taskID uuid.UUID, toolID string, result *ToolResult) {
jsonResult, err := json.MarshalIndent(result, "", " ")
if err != nil {
slog.ErrorContext(ctx, "failed to marshal interpreter result", "error", err)
Expand Down
26 changes: 26 additions & 0 deletions backend/agent/types.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package agent

import (
tooltypes "github.com/furisto/construct/backend/tool/types"
)

// ProviderData contains provider-specific metadata for tool calls.
type ProviderData struct {
Kind string `json:"kind"` // "anthropic", "openai", "gemini"
ID string `json:"id"` // Provider's tool call ID
}

// ToolCall represents a tool invocation with typed input.
type ToolCall struct {
Tool string `json:"tool"`
Input *tooltypes.ToolInput `json:"input,omitempty"`
Provider *ProviderData `json:"provider"`
}

// ToolResult represents a tool result with typed output.
type ToolResult struct {
Tool string `json:"tool"`
Output *tooltypes.ToolOutput `json:"output,omitempty"`
Succeeded bool `json:"succeeded"`
Provider *ProviderData `json:"provider"`
}
8 changes: 3 additions & 5 deletions backend/memory/schema/types/message.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,9 @@ package types
type MessageBlockKind string

const (
MessageBlockKindText MessageBlockKind = "text"
MessageBlockKindNativeToolCall MessageBlockKind = "native_tool_call"
MessageBlockKindNativeToolResult MessageBlockKind = "native_tool_result"
MessageBlockKindCodeInterpreterCall MessageBlockKind = "code_interpreter_call"
MessageBlockKindCodeInterpreterResult MessageBlockKind = "code_interpreter_result"
MessageBlockKindText MessageBlockKind = "text"
MessageBlockKindToolCall MessageBlockKind = "tool_call"
MessageBlockKindToolResult MessageBlockKind = "tool_result"
)

type MessageContent struct {
Expand Down
4 changes: 0 additions & 4 deletions backend/tool/base/interfaces.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,3 @@ type ValidationError struct {
func (e ValidationError) Error() string {
return e.Field + ": " + e.Message
}

type ToolResult interface {
Kind() string
}
Loading
Loading