Skip to content

Commit ef8ea1b

Browse files
Antigravity Agentclaude
andcommitted
feat(queen): human voice for Telegram communication
- Fix "Celebrating farm progress" when farm is offline (0/104 active) - Add context-aware messaging: getContextualReason() explains WHY - Fix notify trigger: only celebrate when training is actually active - Remove experience_save auto-action (was called without --task param) - Add human voice templates: "Farm is sleeping" vs "Farm: 0/104" - Add moodLabelWithExplanation() for state-aware mood - Enhance speak() function with human-like reports Before: "Queen CALM — Cycle #15" / "Celebrating farm progress" After: "🧠 Farm Status Update" / "Farm is sleeping. Last training complete" Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 67c5944 commit ef8ea1b

3 files changed

Lines changed: 224 additions & 84 deletions

File tree

src/tri/queen_actions.zig

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -126,12 +126,9 @@ pub fn desiredAction(state: *const qt.QueenState, senses: qt.SenseResult) ?Actio
126126
if (senses.stale_arena_hours > 24) {
127127
return .arena_battle;
128128
}
129-
// Rule 8: Experience episodes grew → save
130-
if (senses.experience_count > 0) {
131-
// Save periodically (this is a heuristic — fires once per cycle if episodes exist)
132-
return .experience_save;
133-
}
134-
// Rule 9: Farm idle > 3 services → recycle (L2)
129+
// NOTE: experience_save removed from auto-actions — requires --task parameter
130+
// Should only be called manually or after specific error-fixing events
131+
// Rule 8: Farm idle > 3 services → recycle (L2)
135132
if (senses.farm_idle_count > 3) {
136133
return .farm_recycle;
137134
}

src/tri/queen_dlpfc.zig

Lines changed: 177 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -399,8 +399,10 @@ pub fn decide(ctx: *DecisionContext) !?Decision {
399399
});
400400
}
401401

402-
// Rule 3: Best PPL record → celebrate (low urgency, just notification)
403-
if (ctx.farm.best_ppl < 5.0) {
402+
// Rule 3: Active farm with good PPL → celebrate (only if training is happening!)
403+
const has_active_training = ctx.farm.active > 0;
404+
const has_good_ppl = ctx.farm.best_ppl > 0.0 and ctx.farm.best_ppl < 10.0;
405+
if (has_active_training and has_good_ppl) {
404406
try candidates.append(.{
405407
.kind = .notify,
406408
.urgency = .normal,
@@ -445,13 +447,7 @@ pub fn decide(ctx: *DecisionContext) !?Decision {
445447
}
446448
}
447449

448-
const reason = switch (action) {
449-
.doctor_quick => "Build broken, needs healing",
450-
.farm_recycle => "Farm has idle/crashed workers",
451-
.notify => "Celebrating farm progress",
452-
.cloud_spawn => "Agent spawn issues detected",
453-
else => "Routine action",
454-
};
450+
const reason = getContextualReason(action, ctx);
455451

456452
return Decision{
457453
.action = action,
@@ -520,84 +516,196 @@ pub fn act(ctx: *DecisionContext, decision: Decision) !qt.ActionResult {
520516
return result;
521517
}
522518

519+
// ═══════════════════════════════════════════════════════════════════════════════
520+
// CONTEXT-AWARE MESSAGING — Human-like voice
521+
// ═══════════════════════════════════════════════════════════════════════════════
522+
523+
/// Get contextual reason that explains WHAT and WHY
524+
fn getContextualReason(action: qt.ActionKind, ctx: *const DecisionContext) []const u8 {
525+
return switch (action) {
526+
.doctor_quick => buildBrokenReason(ctx),
527+
.farm_recycle => farmRecycleReason(ctx),
528+
.notify => celebrationReason(ctx),
529+
.cloud_spawn => cloudSpawnReason(ctx),
530+
else => "Routine action",
531+
};
532+
}
533+
534+
/// Explain why build healing is needed
535+
fn buildBrokenReason(ctx: *const DecisionContext) []const u8 {
536+
if (ctx.faculty_metrics) |m| {
537+
if (m.compile_rate < 50) {
538+
return "Compile rate very low, needs immediate attention";
539+
}
540+
if (m.dirty_files > 100) {
541+
return "Too many dirty files, build unstable";
542+
}
543+
}
544+
return "Build broken, running quick heal";
545+
}
546+
547+
/// Explain farm recycling decision
548+
fn farmRecycleReason(ctx: *const DecisionContext) []const u8 {
549+
const crashed = ctx.farm.crashed;
550+
const total = ctx.farm.total_services;
551+
const active = ctx.farm.active;
552+
const idle = total - active - crashed;
553+
554+
if (crashed > 5) {
555+
return "Many workers crashed, recycling farm";
556+
}
557+
if (idle > 10) {
558+
return "Many idle workers, recycling for efficiency";
559+
}
560+
return "Farm needs optimization";
561+
}
562+
563+
/// Explain celebration (only when there's something to celebrate!)
564+
fn celebrationReason(ctx: *const DecisionContext) []const u8 {
565+
if (ctx.farm.active == 0) {
566+
return "Farm is idle - nothing to celebrate";
567+
}
568+
569+
const ppl = ctx.farm.best_ppl;
570+
if (ppl < 3.0) {
571+
return "Excellent PPL achieved!";
572+
}
573+
if (ppl < 5.0) {
574+
return "Good progress on PPL";
575+
}
576+
return "Training running smoothly";
577+
}
578+
579+
/// Explain cloud spawn decision
580+
fn cloudSpawnReason(ctx: *const DecisionContext) []const u8 {
581+
const issues = ctx.issues.agent_spawn;
582+
const finished = if (ctx.farm.total_services > 0) ctx.farm.total_services else 0;
583+
584+
if (issues > 3) {
585+
return "Multiple agent spawn issues detected";
586+
}
587+
if (finished > 0) {
588+
return "Spawning replacements for finished containers";
589+
}
590+
return "Agent spawn needed";
591+
}
592+
523593
// ═══════════════════════════════════════════════════════════════════════════════
524594
// SPEAK PHASE — Report decision and result via OFC
525595
// ═══════════════════════════════════════════════════════════════════════════════
526596

527597
pub fn speak(ctx: *DecisionContext, decision: ?Decision, result: qt.ActionResult) !void {
528-
const mood = queen_ofc.inferMood(ctx.build_ok, ctx.ouroboros_score, false);
598+
// Generate human-like report
599+
var report_buf: [1536]u8 = undefined;
600+
const report = formatHumanReport(&report_buf, ctx, decision, result) catch return;
529601

530-
var report_buf: [1024]u8 = undefined;
602+
// Send via OFC
603+
ctx.state.cycle +|= 1;
604+
try queen_ofc.send(ctx.allocator, .group, report);
605+
}
606+
607+
/// Format human-like report that explains WHAT happened and WHY
608+
fn formatHumanReport(
609+
buf: []u8,
610+
ctx: *const DecisionContext,
611+
decision: ?Decision,
612+
result: qt.ActionResult,
613+
) ![]const u8 {
531614
var offset: usize = 0;
532615

533-
// Header
534-
const header = std.fmt.bufPrint(
535-
report_buf[offset..],
536-
"{s} Queen {s} — Cycle #{d}\n\n",
537-
.{ mood.emoji(), mood.label(), ctx.state.cycle },
538-
) catch return;
539-
offset += header.len;
540-
541-
// Farm status
542-
const farm_line = std.fmt.bufPrint(
543-
report_buf[offset..],
544-
"{s} Farm: {d}/{d} active, PPL {d:.1}",
545-
.{ qt.E_DNA, ctx.farm.active, ctx.farm.total_services, ctx.farm.best_ppl },
546-
) catch return;
547-
offset += farm_line.len;
548-
549-
if (ctx.farm.best_ppl_service_len > 0) {
550-
const best_line = std.fmt.bufPrint(
551-
report_buf[offset..],
552-
" ({s})\n",
553-
.{ctx.farm.bestPplServiceStr()},
554-
) catch return;
555-
offset += best_line.len;
556-
} else {
557-
const newline = "\n";
558-
if (offset + newline.len <= report_buf.len) {
559-
@memcpy(report_buf[offset..][0..newline.len], newline);
560-
offset += newline.len;
561-
}
616+
// Context-aware header
617+
const header = getContextualHeader(ctx);
618+
if (offset + header.len <= buf.len) {
619+
@memcpy(buf[offset..][0..header.len], header);
620+
offset += header.len;
562621
}
563622

564-
// Mu heartbeat
565-
const mu_line = std.fmt.bufPrint(
566-
report_buf[offset..],
567-
"{s} Build: {s} | Wake #{d}\n",
568-
.{ qt.E_BRAIN, if (ctx.build_ok) "OK" else "FAIL", ctx.mu_heartbeat.wake },
569-
) catch return;
570-
offset += mu_line.len;
623+
// Farm status (human-readable)
624+
const farm_status = formatFarmStatus(ctx);
625+
if (offset + farm_status.len <= buf.len) {
626+
@memcpy(buf[offset..][0..farm_status.len], farm_status);
627+
offset += farm_status.len;
628+
}
629+
630+
// Build status
631+
const build_status = if (ctx.build_ok)
632+
"\n✅ Build is healthy"
633+
else
634+
"\n❌ Build broken - will attempt healing";
635+
if (offset + build_status.len <= buf.len) {
636+
@memcpy(buf[offset..][0..build_status.len], build_status);
637+
offset += build_status.len;
638+
}
571639

572-
// Decision report
640+
// Action explanation
573641
if (decision) |d| {
574-
const decision_line = std.fmt.bufPrint(
575-
report_buf[offset..],
576-
"{s} Action: {s} ({s})\n",
577-
.{ d.action.emojiIcon(), d.action.label(), d.reason },
578-
) catch return;
579-
offset += decision_line.len;
580-
581-
// Result
582-
const result_line = std.fmt.bufPrint(
583-
report_buf[offset..],
584-
" Result: {s} ({d}ms)\n",
585-
.{ if (result.success) "OK" else "FAIL", result.duration_ms },
586-
) catch return;
587-
offset += result_line.len;
642+
const action_text = formatActionExplanation(d, result);
643+
if (offset + action_text.len <= buf.len) {
644+
@memcpy(buf[offset..][0..action_text.len], action_text);
645+
offset += action_text.len;
646+
}
588647
} else {
589-
const no_action = "No action needed\n";
590-
if (offset + no_action.len <= report_buf.len) {
591-
@memcpy(report_buf[offset..][0..no_action.len], no_action);
648+
const no_action = "\n\n🧠 Standing by. No action needed.";
649+
if (offset + no_action.len <= buf.len) {
650+
@memcpy(buf[offset..][0..no_action.len], no_action);
592651
offset += no_action.len;
593652
}
594653
}
595654

596-
const report = report_buf[0..offset];
655+
return buf[0..offset];
656+
}
597657

598-
// Send via OFC
599-
ctx.state.cycle +|= 1;
600-
try queen_ofc.sendReport(ctx.allocator, mood, report);
658+
/// Get header based on actual system state (not generic mood)
659+
fn getContextualHeader(ctx: *const DecisionContext) []const u8 {
660+
const farm_active = ctx.farm.active;
661+
const farm_total = ctx.farm.total_services;
662+
663+
if (farm_active == 0 and farm_total > 0) {
664+
// Farm is idle
665+
return "🧠 Farm Status Update\n\nTraining farm is idle. ";
666+
} else if (farm_active > 0) {
667+
// Farm is running
668+
return "🧠 Farm Status Update\n\nTraining is active. ";
669+
} else {
670+
// No farm at all
671+
return "🧠 System Status\n\n";
672+
}
673+
}
674+
675+
/// Format farm status in human-readable way
676+
fn formatFarmStatus(ctx: *const DecisionContext) []const u8 {
677+
if (ctx.farm.active == 0 and ctx.farm.total_services == 0) {
678+
return "No training services configured.";
679+
}
680+
681+
if (ctx.farm.active == 0) {
682+
if (ctx.farm.best_ppl < 999.0) {
683+
// Has results but not currently training
684+
return "Farm is sleeping. ";
685+
}
686+
return "Farm is offline. ";
687+
}
688+
689+
// Active training
690+
return "Training in progress. ";
691+
}
692+
693+
/// Explain what action was taken and why
694+
fn formatActionExplanation(d: Decision, result: qt.ActionResult) []const u8 {
695+
_ = result; // Available for future enhancement (e.g., show result emoji)
696+
return switch (d.action) {
697+
.doctor_quick => "\n\n🔧 Running quick heal to fix build issues...",
698+
.farm_recycle => "\n\n♻️ Recycling farm to recover idle/crashed workers.",
699+
.notify => "\n\n📊 Status update - all systems nominal.",
700+
.cloud_spawn => "\n\n🚀 Spawning new agent containers.",
701+
.doctor_heal => "\n\n🏥 Running deep heal to recover codebase.",
702+
.git_commit_state => "\n\n💾 Committing state to preserve work.",
703+
.git_push => "\n\n☁️ Pushing changes to remote.",
704+
.issue_comment => "\n\n📝 Updating GitHub issue tracker.",
705+
.arena_battle => "\n\n⚔️ Running arena battles for evaluation.",
706+
.ouroboros_cycle => "\n\n🔄 Running Ouroboros health cycle.",
707+
else => "\n\nExecuting routine action.",
708+
};
601709
}
602710

603711
/// Format decision report for Telegram

0 commit comments

Comments
 (0)