Skip to content

Commit c6fa5f3

Browse files
Improve error handling
• Use model parameter instead of hardcoded Claude model in AnthropicProvider • Add error logging for Anthropic API failures with context • Improve error wrapping in daemon run command with descriptive messages • Fix nil error handling in message feed to prevent panics • Fix cmd.Printf to fmt.Fprintf in daemon install for consistent output • Refactor connection check to reuse API client instead of creating new one • Update daemon install test expectations for new output format and error messages • Clean up test mocks to remove unnecessary model provider data Co-authored-by: construct-agent <noreply@construct.sh>
1 parent 1d306e9 commit c6fa5f3

6 files changed

Lines changed: 51 additions & 84 deletions

File tree

backend/model/anthropic_provider.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"errors"
66
"fmt"
7+
"log/slog"
78
"time"
89

910
"github.com/anthropics/anthropic-sdk-go"
@@ -76,9 +77,8 @@ func (p *AnthropicProvider) InvokeModel(ctx context.Context, model, systemPrompt
7677
}
7778

7879
request := anthropic.MessageNewParams{
79-
Model: anthropic.ModelClaudeSonnet4_5_20250929,
80-
MaxTokens: modelProfile.MaxTokens,
81-
// Thinking: anthropic.ThinkingConfigParamOfEnabled(int64(float64(modelProfile.MaxTokens) * 0.8)),
80+
Model: anthropic.Model(model),
81+
MaxTokens: modelProfile.MaxTokens,
8282
System: []anthropic.TextBlockParam{
8383
{
8484
Text: systemPrompt,
@@ -127,6 +127,7 @@ func (p *AnthropicProvider) invokeInternal(ctx context.Context, request anthropi
127127
}
128128

129129
if stream.Err() != nil {
130+
slog.ErrorContext(ctx, "failed to invoke model", "error", stream.Err(), "provider", "anthropic")
130131
p.circuitBreaker.RecordResult(stream.Err())
131132
err := p.mapError(stream.Err())
132133
if err.retryableInternal() {

frontend/cli/cmd/daemon_install.go

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ func NewDaemonInstallCmd() *cobra.Command {
5353
Use: "install [flags]",
5454
Short: "Install and enable the Construct daemon as a system service",
5555
Args: cobra.NoArgs,
56-
Long: `Install and enable the Construct daemon as a system service.
56+
Long: `Install and enable the Construct daemon as a system service.
5757
5858
Installs the daemon using the appropriate service manager for your OS (e.g., launchd
5959
on macOS, systemd on Linux). The daemon is required for most construct operations.`,
@@ -77,7 +77,8 @@ on macOS, systemd on Linux). The daemon is required for most construct operation
7777
return err
7878
}
7979

80-
setupComplete, err := checkConnectionAndSetupStatus(cmd.Context(), out, *endpointContext)
80+
client := getAPIClient(cmd.Context())
81+
setupComplete, err := checkConnectionAndSetupStatus(cmd.Context(), out, *endpointContext, client)
8182
if err != nil {
8283
troubleshooting := buildTroubleshootingMessage(cmd.Context(), endpointContext)
8384
return fail.NewUserFacingError(fmt.Sprintf("Connection to daemon failed: %s", err), err, troubleshooting, "",
@@ -87,9 +88,9 @@ on macOS, systemd on Linux). The daemon is required for most construct operation
8788
fmt.Fprintf(out, "%s Daemon installed successfully\n", terminal.SuccessSymbol)
8889

8990
if setupComplete {
90-
cmd.Printf("%s Ready to use! Try 'construct new' to start a conversation\n", terminal.ContinueSymbol)
91+
fmt.Fprintf(out, "%s Ready to use! Try 'construct new' to start a conversation\n", terminal.ContinueSymbol)
9192
} else {
92-
cmd.Printf("%s Next: Create a model provider with 'construct modelprovider create'\n", terminal.ContinueSymbol)
93+
fmt.Fprintf(out, "%s Next: Create a model provider with 'construct modelprovider create'\n", terminal.ContinueSymbol)
9394
}
9495

9596
return nil
@@ -339,16 +340,11 @@ func createOrUpdateContext(ctx context.Context, out io.Writer, socketType string
339340
return &endpointContext, nil
340341
}
341342

342-
func checkConnectionAndSetupStatus(ctx context.Context, out io.Writer, endpoint api.EndpointContext) (bool, error) {
343+
func checkConnectionAndSetupStatus(ctx context.Context, out io.Writer, endpoint api.EndpointContext, client *api.Client) (bool, error) {
343344
canConnect, err := terminal.SpinnerFunc(
344345
out,
345346
"Checking connection to daemon",
346347
func() (bool, error) {
347-
client, err := api.NewClient(endpoint)
348-
if err != nil {
349-
return false, fmt.Errorf("failed to create api client: %w", err)
350-
}
351-
352348
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
353349
defer cancel()
354350
resp, err := client.ModelProvider().ListModelProviders(ctx, &connect.Request[v1.ListModelProvidersRequest]{

frontend/cli/cmd/daemon_install_test.go

Lines changed: 10 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import (
99
v1 "github.com/furisto/construct/api/go/v1"
1010
"github.com/furisto/construct/frontend/cli/cmd/mocks"
1111
"github.com/furisto/construct/shared/conv"
12-
"github.com/google/uuid"
1312
"github.com/spf13/afero"
1413
"go.uber.org/mock/gomock"
1514
)
@@ -37,7 +36,7 @@ func TestDaemonInstall(t *testing.T) {
3736
fs.WriteFile("/usr/local/bin/construct", []byte("binary"), 0755)
3837
},
3938
Expected: TestExpectation{
40-
Stdout: conv.Ptr("Socket file written to /etc/systemd/system/construct.socket\nService file written to /etc/systemd/system/construct.service\nSystemd daemon reloaded\nSocket enabled\n Context 'default' created\n Daemon installed successfully\n Next: Create a model provider with 'construct modelprovider create'\n"),
39+
Stdout: conv.Ptr("Socket file written to /etc/systemd/system/construct.socket\nService file written to /etc/systemd/system/construct.service\nSystemd daemon reloaded\nSocket enabled\n Context 'default' created\n\r\x1b[K✔ Daemon is responding to requests\n✔ Daemon installed successfully\n➡️ Next: Create a model provider with 'construct modelprovider create'\n"),
4140
},
4241
},
4342
{
@@ -59,12 +58,12 @@ func TestDaemonInstall(t *testing.T) {
5958
fs.WriteFile("/usr/local/bin/construct", []byte("binary"), 0755)
6059
},
6160
Expected: TestExpectation{
62-
Stdout: conv.Ptr(" Service file written to /Users/testuser/Library/LaunchAgents/construct-default.plist\n Launchd service loaded\n Context 'default' created\n Daemon installed successfully\n Next: Create a model provider with 'construct modelprovider create'\n"),
61+
Stdout: conv.Ptr(" Service file written to /Users/testuser/Library/LaunchAgents/construct-default.plist\n Launchd service loaded\n Context 'default' created\n\r\x1b[K✔ Daemon is responding to requests\n✔ Daemon installed successfully\n➡️ Next: Create a model provider with 'construct modelprovider create'\n"),
6362
},
6463
},
6564
{
6665
Name: "success - HTTP socket install",
67-
Command: []string{"daemon", "install", "--listen-http", "127.0.0.1:8080"},
66+
Command: []string{"daemon", "install", "--listen-http", "http://127.0.0.1:8080"},
6867
Platform: "linux",
6968
SetupMocks: func(mockClient *api_client.MockClient) {
7069
setupConnectionCheckMock(mockClient, true)
@@ -80,7 +79,7 @@ func TestDaemonInstall(t *testing.T) {
8079
fs.WriteFile("/usr/local/bin/construct", []byte("binary"), 0755)
8180
},
8281
Expected: TestExpectation{
83-
Stdout: conv.Ptr("Socket file written to /etc/systemd/system/construct.socket\nService file written to /etc/systemd/system/construct.service\nSystemd daemon reloaded\nSocket enabled\n Context 'default' created\n Daemon installed successfully\n Next: Create a model provider with 'construct modelprovider create'\n"),
82+
Stdout: conv.Ptr("Socket file written to /etc/systemd/system/construct.socket\nService file written to /etc/systemd/system/construct.service\nSystemd daemon reloaded\nSocket enabled\n Context 'default' created\n\r\x1b[K✔ Daemon is responding to requests\n✔ Daemon installed successfully\n➡️ Next: Create a model provider with 'construct modelprovider create'\n"),
8483
},
8584
},
8685
{
@@ -101,7 +100,7 @@ func TestDaemonInstall(t *testing.T) {
101100
fs.WriteFile("/usr/local/bin/construct", []byte("binary"), 0755)
102101
},
103102
Expected: TestExpectation{
104-
Stdout: conv.Ptr("Socket file written to /etc/systemd/system/construct.socket\nService file written to /etc/systemd/system/construct.service\nSystemd daemon reloaded\nSocket enabled\n Context 'production' created\n Daemon installed successfully\n Next: Create a model provider with 'construct modelprovider create'\n"),
103+
Stdout: conv.Ptr("Socket file written to /etc/systemd/system/construct.socket\nService file written to /etc/systemd/system/construct.service\nSystemd daemon reloaded\nSocket enabled\n Context 'production' created\n\r\x1b[K✔ Daemon is responding to requests\n✔ Daemon installed successfully\n➡️ Next: Create a model provider with 'construct modelprovider create'\n"),
105104
},
106105
},
107106
{
@@ -125,7 +124,7 @@ func TestDaemonInstall(t *testing.T) {
125124
fs.WriteFile("/etc/systemd/system/construct.service", []byte("existing"), 0644)
126125
},
127126
Expected: TestExpectation{
128-
Stdout: conv.Ptr("Socket file written to /etc/systemd/system/construct.socket\nService file written to /etc/systemd/system/construct.service\nSystemd daemon reloaded\nSocket enabled\n Context 'default' created\n Daemon installed successfully\n Next: Create a model provider with 'construct modelprovider create'\n"),
127+
Stdout: conv.Ptr("Socket file written to /etc/systemd/system/construct.socket\nService file written to /etc/systemd/system/construct.service\nSystemd daemon reloaded\nSocket enabled\n Context 'default' created\n\r\x1b[K✔ Daemon is responding to requests\n✔ Daemon installed successfully\n➡️ Next: Create a model provider with 'construct modelprovider create'\n"),
129128
},
130129
},
131130
{
@@ -162,23 +161,7 @@ func TestDaemonInstall(t *testing.T) {
162161
fs.WriteFile("/etc/systemd/system/construct.socket", []byte("existing"), 0644)
163162
},
164163
Expected: TestExpectation{
165-
Error: "Construct daemon is already installed on this system",
166-
},
167-
},
168-
{
169-
Name: "error - permission denied",
170-
Command: []string{"daemon", "install"},
171-
Platform: "linux",
172-
SetupUserInfo: func(userInfo *mocks.MockUserInfo) {
173-
userInfo.EXPECT().ConstructConfigDir().Return("/home/user/.construct", nil).AnyTimes()
174-
},
175-
SetupFileSystem: func(fs *afero.Afero) {
176-
fs.WriteFile("/usr/local/bin/construct", []byte("binary"), 0755)
177-
// Make /etc read-only to simulate permission error
178-
fs.Chmod("/etc", 0444)
179-
},
180-
Expected: TestExpectation{
181-
Error: "Permission denied accessing /etc/systemd/system/construct.socket",
164+
Error: "Construct daemon is already installed on this system\n\nTroubleshooting steps:\n 1. Use '--force' flag to overwrite: construct daemon install --force\n 2. Uninstall first: construct daemon uninstall && construct daemon install\n 3. Use '--name' flag to create a separate daemon instance (advanced)\n\nTechnical details:\nService file exists at: /etc/systemd/system/construct.socket\nIf the problem persists:\n→ https://docs.construct.sh/daemon/troubleshooting#already-installed\n→ https://github.com/furisto/construct/issues/new\n",
182165
},
183166
},
184167
{
@@ -195,7 +178,7 @@ func TestDaemonInstall(t *testing.T) {
195178
fs.WriteFile("/usr/local/bin/construct", []byte("binary"), 0755)
196179
},
197180
Expected: TestExpectation{
198-
Error: "Command failed: systemctl daemon-reload",
181+
Error: "Command failed: systemctl daemon-reload\n\nTroubleshooting steps:\n 1. Check if the required system service is running\n 2. Verify you have permission to manage system services\n 3. Check system logs for more details\n 4. Try running the command manually to diagnose the issue\n\nTechnical details:\nCommand 'systemctl daemon-reload ' failed: systemctl error\nOutput: Failed to reload\nIf the problem persists:\n→ https://docs.construct.sh/daemon/troubleshooting#command-failed\n→ https://github.com/furisto/construct/issues/new\n",
199182
},
200183
},
201184
{
@@ -216,7 +199,7 @@ func TestDaemonInstall(t *testing.T) {
216199
fs.WriteFile("/usr/local/bin/construct", []byte("binary"), 0755)
217200
},
218201
Expected: TestExpectation{
219-
Error: "Connection to daemon failed: failed to check connection: connection failed",
202+
Error: "Connection to daemon failed: failed to check connection: connection failed\n\nTroubleshooting steps:\n 1. Check if the daemon socket is active:\n systemctl --user status construct.socket\n systemctl --user status construct.service\n\n 2. Check service logs:\n journalctl --user -u construct.service --no-pager -n 20\n journalctl --user -u construct.socket --no-pager -n 20\n\n 3. Try manually starting the socket:\n systemctl --user start construct.socket\n systemctl --user start construct.service\n\n 4. Verify the daemon endpoint:\n Address: /tmp/construct-default.sock\n Type: unix\n Check if socket file exists and has correct permissions:\n ls -la /tmp/construct.sock\n\n 5. Check for permission issues:\n # Check if systemd files exist:\n ls -la /etc/systemd/system/construct.*\n\n 6. Try reinstalling the daemon:\n construct daemon uninstall\n construct daemon install\n\n 7. For additional help:\n - Check if the construct binary is accessible and executable\n - Verify system resources (disk space, memory)\n - Run 'construct daemon run' manually to see direct error output\n\n\nIf the problem persists:\n→ https://docs.construct.sh/daemon/troubleshooting\n",
220203
},
221204
},
222205
{
@@ -245,18 +228,7 @@ func setupConnectionCheckMock(mockClient *api_client.MockClient, success bool) {
245228
},
246229
).Return(&connect.Response[v1.ListModelProvidersResponse]{
247230
Msg: &v1.ListModelProvidersResponse{
248-
ModelProviders: []*v1.ModelProvider{
249-
{
250-
Metadata: &v1.ModelProviderMetadata{
251-
Id: uuid.New().String(),
252-
ProviderType: v1.ModelProviderType_MODEL_PROVIDER_TYPE_OPENAI,
253-
},
254-
Spec: &v1.ModelProviderSpec{
255-
Name: "openai",
256-
Enabled: true,
257-
},
258-
},
259-
},
231+
ModelProviders: []*v1.ModelProvider{},
260232
},
261233
}, nil)
262234
} else {

frontend/cli/cmd/daemon_run.go

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -41,23 +41,23 @@ debugging and development. For normal use, 'construct daemon install' is recomme
4141

4242
dataDir, err := userInfo.ConstructDataDir()
4343
if err != nil {
44-
return err
44+
return fmt.Errorf("failed to get construct data directory: %w", err)
4545
}
4646

4747
db, err := memory.Open(dialect.SQLite, "file:"+filepath.Join(dataDir, "construct.db")+"?_pragma=foreign_keys(1)&_pragma=journal_mode(WAL)&_pragma=busy_timeout(5000)")
4848
if err != nil {
49-
return err
49+
return fmt.Errorf("failed to open database: %w", err)
5050
}
5151
defer db.Close()
52-
52+
5353
err = setupMemory(cmd.Context(), db)
5454
if err != nil {
55-
return err
55+
return fmt.Errorf("failed to setup memory/database schema: %w", err)
5656
}
5757

5858
encryption, err := getEncryptionClient()
5959
if err != nil {
60-
return err
60+
return fmt.Errorf("failed to get encryption client: %w", err)
6161
}
6262

6363
provider, err := listener.DetectProvider(options.HTTPAddress, options.UnixSocket)
@@ -81,7 +81,7 @@ debugging and development. For normal use, 'construct daemon install' is recomme
8181

8282
analytics, err := analytics.NewPostHogClient()
8383
if err != nil {
84-
return err
84+
return fmt.Errorf("failed to create analytics client: %w", err)
8585
}
8686

8787
runtime, err := agent.NewRuntime(
@@ -103,11 +103,15 @@ debugging and development. For normal use, 'construct daemon install' is recomme
103103
)
104104

105105
if err != nil {
106-
return err
106+
return fmt.Errorf("failed to create agent runtime: %w", err)
107107
}
108108

109109
fmt.Fprintf(cmd.OutOrStdout(), "🤖 Starting Agent Runtime...\n")
110-
return runtime.Run(cmd.Context())
110+
err = runtime.Run(cmd.Context())
111+
if err != nil {
112+
return fmt.Errorf("failed to run agent runtime: %w", err)
113+
}
114+
return nil
111115
},
112116
}
113117

@@ -131,28 +135,28 @@ func getEncryptionClient() (*secret.Client, error) {
131135
keyHandleJson, err := secret.GetSecret[string](secret.ModelProviderEncryptionKey())
132136
if err != nil {
133137
if !errors.Is(err, &secret.ErrSecretNotFound{}) {
134-
return nil, err
138+
return nil, fmt.Errorf("failed to get encryption key secret: %w", err)
135139
}
136140

137141
slog.Debug("generating new encryption key")
138142
keyHandle, err = secret.GenerateKeyset()
139143
if err != nil {
140-
return nil, err
144+
return nil, fmt.Errorf("failed to generate encryption keyset: %w", err)
141145
}
142146
keysetJson, err := secret.KeysetToJSON(keyHandle)
143147
if err != nil {
144-
return nil, err
148+
return nil, fmt.Errorf("failed to convert keyset to JSON: %w", err)
145149
}
146150

147151
err = secret.SetSecret(secret.ModelProviderEncryptionKey(), &keysetJson)
148152
if err != nil {
149-
return nil, err
153+
return nil, fmt.Errorf("failed to store encryption key secret: %w", err)
150154
}
151155
} else {
152156
slog.Debug("loading encryption key")
153157
keyHandle, err = secret.KeysetFromJSON(*keyHandleJson)
154158
if err != nil {
155-
return nil, err
159+
return nil, fmt.Errorf("failed to load encryption keyset from JSON: %w", err)
156160
}
157161
}
158162

frontend/cli/pkg/terminal/message_feed.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,10 @@ func linesFromBottom(vp viewport.Model) int {
173173
}
174174

175175
func (m *MessageFeed) upsertErrorMessage(errMsg *Error) {
176+
if errMsg.Error == nil {
177+
return
178+
}
179+
176180
if len(m.messages) > 0 {
177181
if lastMsg, ok := m.messages[len(m.messages)-1].(*Error); ok {
178182
lastMsg.Error = errMsg.Error
@@ -374,7 +378,7 @@ func formatMessages(messages []message, partialMessage string, width int) string
374378
case *editFileToolCall:
375379
renderedMessages = append(renderedMessages, renderToolCallMessage("Edit", msg.Input.Path, width, addBottomMargin(i, messages)))
376380

377-
case *executeCommandToolCall:
381+
case *executeCommandToolCall:
378382
renderedMessages = append(renderedMessages, renderToolCallMessage("Execute", msg.Input.Command, width, addBottomMargin(i, messages)))
379383

380384
case *findFileToolCall:

0 commit comments

Comments
 (0)