Skip to content

Commit 86ea11d

Browse files
Unify tool call and result persistence format (#81)
Replace 4 legacy message block kinds with 2 unified kinds (tool_call, tool_result) and introduce typed Input/Output structures with provider metadata. Changes: - Add ProviderData, ToolCall, ToolResult types in agent package - Add InterpreterInput, InterpreterOutput, FunctionCall to tool/types - Make codeact types aliases for tooltypes equivalents - Update Interpreter.Interpret() to take *InterpreterInput directly - Replace legacy block kinds in memory/schema/types - Update conversion layer to handle unified format - Simplify task_reconciler to use unified types throughout Co-authored-by: construct-agent <noreply@construct.sh>
1 parent e61a5ce commit 86ea11d

10 files changed

Lines changed: 1024 additions & 1038 deletions

File tree

backend/agent/conv.go

Lines changed: 456 additions & 408 deletions
Large diffs are not rendered by default.

backend/agent/task_reconciler.go

Lines changed: 63 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,9 @@ import (
2121
"github.com/furisto/construct/backend/model"
2222
"github.com/furisto/construct/backend/prompt"
2323
"github.com/furisto/construct/backend/skill"
24-
"github.com/furisto/construct/backend/tool/base"
2524
"github.com/furisto/construct/backend/tool/codeact"
25+
tooltypes "github.com/furisto/construct/backend/tool/types"
2626
"github.com/furisto/construct/shared"
27-
"github.com/furisto/construct/shared/conv"
2827
"github.com/google/uuid"
2928
"github.com/prometheus/client_golang/prometheus"
3029
"github.com/spf13/afero"
@@ -718,63 +717,79 @@ func (r *TaskReconciler) reconcileExecuteTools(ctx context.Context, taskID uuid.
718717
return Result{Retry: true}, nil
719718
}
720719

721-
func (r *TaskReconciler) callTools(ctx context.Context, task *memory.Task, message *memory.Message) ([]base.ToolResult, map[string]int64, error) {
720+
func (r *TaskReconciler) callTools(ctx context.Context, task *memory.Task, message *memory.Message) ([]*ToolResult, map[string]int64, error) {
722721
logger := r.logger.With(
723722
KeyTaskID, task.ID,
724723
KeyMessageID, message.ID,
725724
)
726725
LogOperationStart(logger, "call tools")
727726

728-
var toolResults []base.ToolResult
727+
var toolResults []*ToolResult
729728
toolStats := make(map[string]int64)
730729

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

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

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

753-
success := err == nil
754-
if !success {
755-
LogError(logger, "code interpreter execution failed", err, KeyToolDuration, toolDuration.Milliseconds())
756-
} else {
757-
logger.DebugContext(ctx, "code interpreter execution completed",
758-
"duration_ms", toolDuration.Milliseconds(),
759-
"success", true,
760-
)
761-
}
762-
interpreterResult := &codeact.InterpreterToolResult{
763-
ID: toolCall.ID,
764-
Output: result.ConsoleOutput,
765-
FunctionCalls: result.FunctionCalls,
766-
Error: conv.ErrorToString(err),
767-
}
768-
toolResults = append(toolResults, interpreterResult)
769-
770-
for tool, count := range result.ToolStats {
771-
toolStats[tool] += count
772-
logger.DebugContext(ctx, "tool invoked",
773-
KeyToolName, tool,
774-
"count", count,
775-
)
752+
if errors.Is(ctx.Err(), context.Canceled) {
753+
err = errors.New("tool execution was cancelled by user. Wait for further instructions")
754+
}
755+
756+
success := err == nil
757+
if !success {
758+
LogError(logger, "code interpreter execution failed", err, KeyToolDuration, toolDuration.Milliseconds())
759+
} else {
760+
logger.DebugContext(ctx, "code interpreter execution completed",
761+
"duration_ms", toolDuration.Milliseconds(),
762+
"success", true,
763+
)
764+
}
765+
766+
// Create unified ToolResult directly
767+
toolResult := &ToolResult{
768+
Tool: toolCall.Tool,
769+
Output: &tooltypes.ToolOutput{
770+
Interpreter: &tooltypes.InterpreterOutput{
771+
ConsoleOutput: result.ConsoleOutput,
772+
FunctionCalls: result.FunctionCalls,
773+
ToolStats: result.ToolStats,
774+
},
775+
},
776+
Succeeded: err == nil,
777+
Provider: &ProviderData{
778+
Kind: toolCall.Provider.Kind,
779+
ID: toolCall.Provider.ID,
780+
},
781+
}
782+
toolResults = append(toolResults, toolResult)
783+
784+
for tool, count := range result.ToolStats {
785+
toolStats[tool] += count
786+
logger.DebugContext(ctx, "tool invoked",
787+
KeyToolName, tool,
788+
"count", count,
789+
)
790+
}
791+
logInterpreterResult(ctx, task.ID, toolCall.Provider.ID, toolResult)
776792
}
777-
logInterpreterResult(ctx, task.ID, toolCall.ID, interpreterResult)
778793
}
779794
}
780795

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

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

797-
switch result.(type) {
798-
case *codeact.InterpreterToolResult:
799-
toolBlocks = append(toolBlocks, types.MessageBlock{
800-
Kind: types.MessageBlockKindCodeInterpreterResult,
801-
Payload: string(jsonResult),
802-
})
803-
default:
804-
toolBlocks = append(toolBlocks, types.MessageBlock{
805-
Kind: types.MessageBlockKindNativeToolResult,
806-
Payload: string(jsonResult),
807-
})
808-
}
812+
toolBlocks = append(toolBlocks, types.MessageBlock{
813+
Kind: types.MessageBlockKindToolResult,
814+
Payload: string(jsonResult),
815+
})
809816
}
810817

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

838-
func logInterpreterResult(ctx context.Context, taskID uuid.UUID, toolID string, result *codeact.InterpreterToolResult) {
845+
func logInterpreterResult(ctx context.Context, taskID uuid.UUID, toolID string, result *ToolResult) {
839846
jsonResult, err := json.MarshalIndent(result, "", " ")
840847
if err != nil {
841848
slog.ErrorContext(ctx, "failed to marshal interpreter result", "error", err)

backend/agent/types.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package agent
2+
3+
import (
4+
tooltypes "github.com/furisto/construct/backend/tool/types"
5+
)
6+
7+
// ProviderData contains provider-specific metadata for tool calls.
8+
type ProviderData struct {
9+
Kind string `json:"kind"` // "anthropic", "openai", "gemini"
10+
ID string `json:"id"` // Provider's tool call ID
11+
}
12+
13+
// ToolCall represents a tool invocation with typed input.
14+
type ToolCall struct {
15+
Tool string `json:"tool"`
16+
Input *tooltypes.ToolInput `json:"input,omitempty"`
17+
Provider *ProviderData `json:"provider"`
18+
}
19+
20+
// ToolResult represents a tool result with typed output.
21+
type ToolResult struct {
22+
Tool string `json:"tool"`
23+
Output *tooltypes.ToolOutput `json:"output,omitempty"`
24+
Succeeded bool `json:"succeeded"`
25+
Provider *ProviderData `json:"provider"`
26+
}

backend/memory/schema/types/message.go

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,9 @@ package types
33
type MessageBlockKind string
44

55
const (
6-
MessageBlockKindText MessageBlockKind = "text"
7-
MessageBlockKindNativeToolCall MessageBlockKind = "native_tool_call"
8-
MessageBlockKindNativeToolResult MessageBlockKind = "native_tool_result"
9-
MessageBlockKindCodeInterpreterCall MessageBlockKind = "code_interpreter_call"
10-
MessageBlockKindCodeInterpreterResult MessageBlockKind = "code_interpreter_result"
6+
MessageBlockKindText MessageBlockKind = "text"
7+
MessageBlockKindToolCall MessageBlockKind = "tool_call"
8+
MessageBlockKindToolResult MessageBlockKind = "tool_result"
119
)
1210

1311
type MessageContent struct {

backend/tool/base/interfaces.go

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,3 @@ type ValidationError struct {
5656
func (e ValidationError) Error() string {
5757
return e.Field + ": " + e.Message
5858
}
59-
60-
type ToolResult interface {
61-
Kind() string
62-
}

0 commit comments

Comments
 (0)