Skip to content

Commit b8aab34

Browse files
barckcodeclaude
andcommitted
fix: add provider-aware deployment to scheduler executor
The scheduler executor's deployTeam method was missing provider-aware logic, causing OpenCode teams (especially Google models) to deploy with the wrong container image and empty AGENT_PROVIDER env var. This made the sidecar default to Claude provider, breaking both chat and scheduled tasks for OpenCode teams. Changes: - Pass Provider field to AgentConfig (was missing entirely) - Select correct Docker image based on provider (opencode vs claude) - Set OPENCODE_MODEL / CLAUDE_MODEL env vars appropriately - Generate SubAgentFiles for both Claude and OpenCode providers - Build instructionsMD with team member context - Collect and deduplicate skills from leader and workers - Validate OpenCode credentials before deployment - Add helper functions: schedulerClaudeModelID, schedulerValidateOpenCodeCredentials Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 8421183 commit b8aab34

1 file changed

Lines changed: 247 additions & 7 deletions

File tree

internal/scheduler/executor.go

Lines changed: 247 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -226,7 +226,7 @@ func (e *Executor) deployTeam(ctx context.Context, team models.Team) error {
226226
// Default: update status to deploying and call runtime.
227227
e.DB.Model(&team).Update("status", models.TeamStatusDeploying)
228228

229-
// Simplified deployment — just deploy infrastructure and leader.
229+
// Deploy infrastructure.
230230
infraCfg := runtime.InfraConfig{
231231
TeamName: team.Name,
232232
NATSEnabled: true,
@@ -238,11 +238,30 @@ func (e *Executor) deployTeam(ctx context.Context, team models.Team) error {
238238
return fmt.Errorf("deploying infrastructure: %w", err)
239239
}
240240

241-
// Find the leader.
241+
provider := team.Provider
242+
if provider == "" {
243+
provider = models.ProviderClaude
244+
}
245+
246+
// Load settings from DB for environment variables.
247+
env := map[string]string{}
248+
if e.LoadSettingsEnvFunc != nil {
249+
env = e.LoadSettingsEnvFunc()
250+
}
251+
252+
natsURL := e.Runtime.GetNATSURL(team.Name)
253+
254+
// Find the leader and extract leader skills.
242255
var leader *models.Agent
256+
var leaderSkills json.RawMessage
257+
var leaderSkillConfigs []protocol.SkillConfig
243258
for i := range team.Agents {
244259
if team.Agents[i].Role == models.AgentRoleLeader {
245260
leader = &team.Agents[i]
261+
if len(leader.SubAgentSkills) > 0 && string(leader.SubAgentSkills) != "null" {
262+
leaderSkills = json.RawMessage(leader.SubAgentSkills)
263+
_ = json.Unmarshal(leader.SubAgentSkills, &leaderSkillConfigs)
264+
}
246265
break
247266
}
248267
}
@@ -251,20 +270,200 @@ func (e *Executor) deployTeam(ctx context.Context, team models.Team) error {
251270
return fmt.Errorf("no leader agent found in team")
252271
}
253272

254-
env := map[string]string{}
255-
if e.LoadSettingsEnvFunc != nil {
256-
env = e.LoadSettingsEnvFunc()
273+
// Build team member list for the leader's instructions.
274+
var teamMembers []runtime.TeamMemberInfo
275+
for _, a := range team.Agents {
276+
teamMembers = append(teamMembers, runtime.TeamMemberInfo{
277+
Name: sanitizeTeamName(a.Name),
278+
Role: a.Role,
279+
Specialty: a.Specialty,
280+
})
281+
}
282+
283+
// Generate sub-agent files for workers based on provider.
284+
subAgentFiles := map[string]string{}
285+
var openCodeWorkers []runtime.SubAgentInfo
286+
for i := range team.Agents {
287+
agent := &team.Agents[i]
288+
if agent.Role == models.AgentRoleLeader {
289+
continue
290+
}
291+
292+
if provider == models.ProviderOpenCode {
293+
subInfo := runtime.SubAgentInfo{
294+
Name: agent.Name,
295+
Description: agent.SubAgentDescription,
296+
Model: agent.SubAgentModel,
297+
Skills: json.RawMessage(agent.SubAgentSkills),
298+
ClaudeMD: agent.InstructionsMD,
299+
}
300+
filename := runtime.SubAgentFileName(agent.Name)
301+
subAgentFiles[filename] = runtime.GenerateOpenCodeSubAgentContent(subInfo, leaderSkillConfigs)
302+
openCodeWorkers = append(openCodeWorkers, subInfo)
303+
} else {
304+
info := runtime.AgentWorkspaceInfo{
305+
Name: agent.Name,
306+
Role: agent.Role,
307+
Specialty: agent.Specialty,
308+
SystemPrompt: agent.SystemPrompt,
309+
ClaudeMD: agent.InstructionsMD,
310+
Skills: json.RawMessage(agent.Skills),
311+
}
312+
subInfo := runtime.SubAgentInfo{
313+
Name: agent.Name,
314+
Description: agent.SubAgentDescription,
315+
Model: agent.SubAgentModel,
316+
Skills: json.RawMessage(agent.SubAgentSkills),
317+
GlobalSkills: leaderSkills,
318+
ClaudeMD: agent.InstructionsMD,
319+
}
320+
if subInfo.ClaudeMD == "" {
321+
subInfo.ClaudeMD = runtime.GenerateClaudeMD(info)
322+
}
323+
filename := runtime.SubAgentFileName(agent.Name)
324+
subAgentFiles[filename] = runtime.GenerateSubAgentContent(subInfo)
325+
326+
if team.WorkspacePath != "" {
327+
if _, err := runtime.SetupSubAgentFile(team.WorkspacePath, subInfo); err != nil {
328+
slog.Error("executor: failed to setup sub-agent file", "agent", agent.Name, "error", err)
329+
}
330+
}
331+
}
332+
}
333+
334+
// Setup host workspace for the leader based on provider.
335+
if team.WorkspacePath != "" {
336+
if provider == models.ProviderOpenCode {
337+
leaderSub := runtime.SubAgentInfo{
338+
Name: leader.Name,
339+
Description: leader.Specialty,
340+
Skills: json.RawMessage(leader.Skills),
341+
ClaudeMD: leader.InstructionsMD,
342+
}
343+
if err := runtime.SetupOpenCodeWorkspace(team.WorkspacePath, team.Name, leaderSub, openCodeWorkers, leaderSkillConfigs); err != nil {
344+
slog.Error("executor: failed to setup opencode workspace", "team", team.Name, "error", err)
345+
}
346+
} else {
347+
info := runtime.AgentWorkspaceInfo{
348+
Name: leader.Name,
349+
Role: leader.Role,
350+
Specialty: leader.Specialty,
351+
SystemPrompt: leader.SystemPrompt,
352+
ClaudeMD: leader.InstructionsMD,
353+
Skills: json.RawMessage(leader.Skills),
354+
TeamMembers: teamMembers,
355+
}
356+
if _, err := runtime.SetupAgentWorkspace(team.WorkspacePath, info); err != nil {
357+
slog.Error("executor: failed to setup agent workspace", "agent", leader.Name, "error", err)
358+
}
359+
}
360+
}
361+
362+
// Collect all unique skills from all agents for sidecar installation.
363+
type skillKey struct{ RepoURL, SkillName string }
364+
skillsSet := map[skillKey]struct{}{}
365+
var allSkills []protocol.SkillConfig
366+
for _, a := range team.Agents {
367+
var agentSkills []protocol.SkillConfig
368+
if err := json.Unmarshal(a.SubAgentSkills, &agentSkills); err == nil {
369+
for _, s := range agentSkills {
370+
key := skillKey{s.RepoURL, s.SkillName}
371+
if s.RepoURL != "" && s.SkillName != "" {
372+
if _, exists := skillsSet[key]; !exists {
373+
skillsSet[key] = struct{}{}
374+
allSkills = append(allSkills, s)
375+
}
376+
}
377+
}
378+
}
379+
}
380+
if len(allSkills) > 0 {
381+
skillsJSON, _ := json.Marshal(allSkills)
382+
env["AGENT_SKILLS_INSTALL"] = string(skillsJSON)
383+
}
384+
385+
// Set model env var based on provider.
386+
leaderModel := leader.SubAgentModel
387+
if leaderModel != "" && leaderModel != "inherit" {
388+
if provider == models.ProviderOpenCode {
389+
env["OPENCODE_MODEL"] = leaderModel
390+
} else {
391+
if fullModel := schedulerClaudeModelID(leaderModel); fullModel != "" {
392+
env["CLAUDE_MODEL"] = fullModel
393+
}
394+
}
395+
} else if provider == models.ProviderOpenCode {
396+
if m := env["OPENCODE_MODEL"]; m != "" {
397+
env["OPENCODE_MODEL"] = m
398+
}
399+
}
400+
401+
// Validate OpenCode model credentials before deployment.
402+
if provider == models.ProviderOpenCode {
403+
effectiveModel := env["OPENCODE_MODEL"]
404+
if err := schedulerValidateOpenCodeCredentials(effectiveModel, env); err != nil {
405+
e.DB.Model(&team).Update("status", models.TeamStatusError)
406+
return fmt.Errorf("credential validation: %w", err)
407+
}
408+
for _, a := range team.Agents {
409+
if a.Role == models.AgentRoleWorker && a.SubAgentModel != "" && a.SubAgentModel != "inherit" {
410+
if err := schedulerValidateOpenCodeCredentials(a.SubAgentModel, env); err != nil {
411+
e.DB.Model(&team).Update("status", models.TeamStatusError)
412+
return fmt.Errorf("credential validation for worker %s: %w", a.Name, err)
413+
}
414+
}
415+
}
416+
}
417+
418+
// Generate leader instructions content based on provider.
419+
var instructionsMDContent string
420+
if provider == models.ProviderOpenCode {
421+
if leader.InstructionsMD != "" {
422+
instructionsMDContent = leader.InstructionsMD
423+
} else {
424+
leaderSubInfo := runtime.SubAgentInfo{
425+
Name: leader.Name,
426+
Description: leader.Specialty,
427+
Skills: json.RawMessage(leader.Skills),
428+
ClaudeMD: leader.InstructionsMD,
429+
}
430+
workers := make([]runtime.SubAgentInfo, 0)
431+
for _, a := range team.Agents {
432+
if a.Role != models.AgentRoleLeader {
433+
workers = append(workers, runtime.SubAgentInfo{
434+
Name: a.Name,
435+
Description: a.SubAgentDescription,
436+
})
437+
}
438+
}
439+
instructionsMDContent = runtime.GenerateOpenCodeAgentsMD(team.Name, leaderSubInfo, workers)
440+
}
441+
} else {
442+
leaderInfo := runtime.AgentWorkspaceInfo{
443+
Name: leader.Name,
444+
Role: leader.Role,
445+
Specialty: leader.Specialty,
446+
SystemPrompt: leader.SystemPrompt,
447+
ClaudeMD: leader.InstructionsMD,
448+
Skills: json.RawMessage(leader.Skills),
449+
TeamMembers: teamMembers,
450+
}
451+
instructionsMDContent = leader.InstructionsMD
452+
if instructionsMDContent == "" {
453+
instructionsMDContent = runtime.GenerateClaudeMD(leaderInfo)
454+
}
257455
}
258456

259-
natsURL := e.Runtime.GetNATSURL(team.Name)
260457
agentCfg := runtime.AgentConfig{
261458
Name: leader.Name,
262459
TeamName: team.Name,
263460
Role: leader.Role,
461+
Provider: provider,
264462
SystemPrompt: leader.SystemPrompt,
265-
ClaudeMD: leader.InstructionsMD,
463+
ClaudeMD: instructionsMDContent,
266464
NATSUrl: natsURL,
267465
WorkspacePath: team.WorkspacePath,
466+
SubAgentFiles: subAgentFiles,
268467
Env: env,
269468
}
270469

@@ -283,6 +482,47 @@ func (e *Executor) deployTeam(ctx context.Context, team models.Team) error {
283482
return nil
284483
}
285484

485+
// schedulerClaudeModelID maps short model names to full Claude model IDs.
486+
func schedulerClaudeModelID(short string) string {
487+
switch short {
488+
case "sonnet":
489+
return "claude-sonnet-4-20250514"
490+
case "opus":
491+
return "claude-opus-4-20250514"
492+
case "haiku":
493+
return "claude-haiku-4-5-20251001"
494+
default:
495+
return ""
496+
}
497+
}
498+
499+
// schedulerValidateOpenCodeCredentials checks that the required API key for the
500+
// given OpenCode model is present in the environment.
501+
func schedulerValidateOpenCodeCredentials(model string, env map[string]string) error {
502+
if model == "" || model == "inherit" {
503+
return nil
504+
}
505+
parts := strings.SplitN(model, "/", 2)
506+
if len(parts) < 2 {
507+
return nil
508+
}
509+
required := map[string]string{
510+
"anthropic": "ANTHROPIC_API_KEY",
511+
"openai": "OPENAI_API_KEY",
512+
"google": "GOOGLE_GENERATIVE_AI_API_KEY",
513+
"ollama": "OLLAMA_BASE_URL",
514+
"lmstudio": "LM_STUDIO_BASE_URL",
515+
}
516+
key, ok := required[parts[0]]
517+
if !ok {
518+
return nil
519+
}
520+
if env[key] == "" {
521+
return fmt.Errorf("missing credential for model %q: set %s in the Settings page", model, key)
522+
}
523+
return nil
524+
}
525+
286526
// stopTeam stops a running team.
287527
func (e *Executor) stopTeam(ctx context.Context, team models.Team) error {
288528
if e.StopTeamFunc != nil {

0 commit comments

Comments
 (0)