This commit is contained in:
Ian Gulliver
2026-10-20 22:02:59 -07:00
parent ccbbb5807f
commit 57324fe9a2
5 changed files with 31 additions and 53 deletions
+1 -1
View File
@@ -241,7 +241,7 @@ For Claude, byre chooses the session ID (`vendor_session_id`) up front. Codex pi
Every session gets byre's guidance (`guidance` in `internal/agents/guidance.go`): ask questions with `ask_human`, end each turn with a final message that summarizes it (including every departure from the plan), put everything the human needs in a question's context since that turn has no summary, don't commit (byre commits each turn), don't push, touch other branches or run git maintenance, and write `TODO.md` items the way byre parses them (see "TODO"): a top-level `- ` line holding only the title, with the description indented under it. A task agent's guidance also names its port range, tells it to do the setup AGENTS.md describes in a new worktree (see "Worktree setup and ports"), and quotes the task's title, which may be all the human typed when creating the task. A task started from a `TODO.md` item gets one more line naming the item and telling the agent to remove it from `TODO.md` in its worktree when the work is done (`taskGuidance`; see "TODO"). A subtask gets one naming its parent and saying that its final messages go to the parent's agent (`subtaskGuidance`; see "Subtasks"). For Claude it is passed as `--append-system-prompt`, which Claude Code records on a session's first request and reuses on resume, so a running session keeps the text it started with; a `UserPromptSubmit` hook also attaches it as `additionalContext` to every user turn, which reaches existing sessions too. For Codex it is the thread's `developerInstructions`, and every `turn/start` also carries it as `additionalContext` under the key `byre`. For Antigravity it is an always-on rule in the session's plugin (`rules/AGENTS.md`, with the notes in `rules/notes.md`), written when byre starts the session's process, and every message also starts with it in `<system-reminder>` tags. The guidance names byre's tools vendor-neutrally ("byre's ask_human tool"): Claude sees them as `mcp__byre__<name>`, Codex as `<name>` in the namespace `byre` (Codex reserves the `mcp__` prefix), Antigravity as `<name>` on the MCP server `byre_byre`, called through its `call_mcp_tool` (recorded as `mcp__byre__<name>`).
A turn whose final message asks the human something in prose is bounced once (`takeBounce` in `internal/agents/guidance.go`): if `ask_human` wasn't called during the turn and the final message's last paragraph has a sentence ending in `?` outside code spans, code blocks, quotes, blockquotes, table rows and `<details>` sections, the agent is told to ask through `ask_human` and carries on. Questions earlier in the message (quoted, answered, or listed for someone else) don't count. A task started as a TODO discussion (its first `user.message` holds `discussPrompt` from `internal/agents/tasks.go`) is never bounced, since that prompt lets the agent talk it over in chat as well as through `ask_human`, and neither is a task with an open question already in the inbox, since the prose then points at that question. A bounce is recorded as a `byre` `user.message` with `mid_turn: true` and `bounce: true` carrying the nudge; the transcript draws neither it nor the agent message before it, so the rewritten message takes the blocked one's place. For Claude the bounce is a Stop hook (`bounceProseQuestion`) that blocks the stop; Claude Code reports the block as a "Stop hook error" notification, which is expected. For Codex, byre doesn't end its turn on `turn/completed`: it records the nudge and starts another Codex turn with it, and only that turn's end ends byre's turn. Antigravity works the same way on its `result`, since its `Stop` hook isn't given the final message. The per-turn flags reset when the next user message is sent. An agent told explicitly to ask in plain text will refuse the bounce.
A turn whose final message asks the human something in prose is bounced once (`takeBounce` in `internal/agents/guidance.go`): if `ask_human` wasn't called during the turn and the final message's last paragraph has a sentence ending in `?` outside code spans, code blocks, quotes, blockquotes, table rows and `<details>` sections, the agent is told to ask through `ask_human` and carries on. Questions earlier in the message (quoted, answered, or listed for someone else) don't count. A task started as a TODO discussion (its first `user.message` holds `discussPrompt` from `internal/agents/tasks.go`) is never bounced, since that prompt lets the agent talk it over in chat as well as through `ask_human`, and neither is a task with an open question already in the inbox, since the prose then points at that question. `takeBounce` records each accepted bounce for every vendor; Codex and Antigravity share `continueProseQuestion`, which clears the blocked final text and sends the nudge through `agentProcess.send`. A bounce is recorded as a `byre` `user.message` with `mid_turn: true` and `bounce: true` carrying the nudge; the transcript draws neither it nor the agent message before it, so the rewritten message takes the blocked one's place. For Claude the bounce is a Stop hook (`bounceProseQuestion`) that blocks the stop; Claude Code reports the block as a "Stop hook error" notification, which is expected. For Codex, byre doesn't end its turn on `turn/completed`: it records the nudge and starts another Codex turn with it, and only that turn's end ends byre's turn. Antigravity works the same way on its `result`, since its `Stop` hook isn't given the final message. The per-turn flags reset when the next user message is sent. An agent told explicitly to ask in plain text will refuse the bounce.
A turn's final message is its summary, and it lives only in the transcript, so the human reads it once. When a task turn ends normally (not an error, not interrupted, not a handoff) with non-empty `result` text, didn't call `ask_human`, and isn't a Merge turn or discard turn (the task closes right after one, so its final message is never left to read), its `turn.ended` event carries `summary: true`. A Merge turn that called `stop_merge` keeps the task open and does carry it. That flag drives the lead's "finished a turn" signal and the "Turn summaries" notification (see "Lead agent" and "Notifications"). A turn that asked a question has no summary, since the final message comes after the question it explains; the guidance tells agents to put everything the human needs in the question's context. There is no report tool: models write a closing recap whatever they are told, so byre uses it rather than asking for a second one.
+2 -22
View File
@@ -163,30 +163,10 @@ func (m *Manager) agyTurnEnded(ctx context.Context, l *live, p antigravityProces
m.mu.Unlock()
success := res.Status == "SUCCESS"
bounce := false
if success && l.implementer() {
var err error
bounce, err = m.takeBounce(ctx, l, final)
if err != nil {
bounce, err := m.continueProseQuestion(ctx, l, p, final, success)
if err != nil || bounce {
return err
}
}
if bounce {
err := m.emit(ctx, l, "byre", "user.message", map[string]any{"text": proseQuestionNudge, "mid_turn": true, "bounce": true})
if err != nil {
return err
}
m.mu.Lock()
l.lastText = ""
m.mu.Unlock()
go func() {
err := p.s.Send(proseQuestionNudge)
if err != nil {
slog.Error("send prose question nudge", "session", l.id, "err", err)
}
}()
return nil
}
r := endResult{
subtype: strings.ToLower(res.Status),
+2 -22
View File
@@ -250,30 +250,10 @@ func (m *Manager) codexTurnCompleted(ctx context.Context, l *live, s *codex.Sess
final := l.lastText
m.mu.Unlock()
bounce := false
if turn.Status == codex.TurnStatusCompleted && l.implementer() {
var err error
bounce, err = m.takeBounce(ctx, l, final)
if err != nil {
bounce, err := m.continueProseQuestion(ctx, l, codexProcess{s: s}, final, turn.Status == codex.TurnStatusCompleted)
if err != nil || bounce {
return err
}
}
if bounce {
err := m.emit(ctx, l, "byre", "user.message", map[string]any{"text": proseQuestionNudge, "mid_turn": true, "bounce": true})
if err != nil {
return err
}
m.mu.Lock()
l.lastText = ""
m.mu.Unlock()
go func() {
_, err := s.Send(context.Background(), proseQuestionNudge)
if err != nil {
slog.Error("send prose question nudge", "session", l.id, "err", err)
}
}()
return nil
}
res := endResult{
subtype: string(turn.Status),
+4 -6
View File
@@ -63,10 +63,6 @@ func (m *Manager) bounceProseQuestion(l *live) claude.HookFunc {
if err != nil || !bounce {
return map[string]any{}, err
}
err = m.emit(ctx, l, "byre", "user.message", map[string]any{"text": proseQuestionNudge, "mid_turn": true, "bounce": true})
if err != nil {
return nil, err
}
return map[string]any{"decision": "block", "reason": proseQuestionNudge}, nil
}
}
@@ -91,12 +87,14 @@ func (m *Manager) takeBounce(ctx context.Context, l *live, final string) (bool,
}
m.mu.Lock()
defer m.mu.Unlock()
if l.asked || l.bounced || l.compacting {
m.mu.Unlock()
return false, nil
}
l.bounced = true
return true, nil
m.mu.Unlock()
err = m.emit(ctx, l, "byre", "user.message", map[string]any{"text": proseQuestionNudge, "mid_turn": true, "bounce": true})
return err == nil, err
}
func (m *Manager) taskGuidance(ctx context.Context, task api.Task) (string, error) {
+20
View File
@@ -324,3 +324,23 @@ func (p antigravityProcess) drive(m *Manager, l *live) {
}
}
}
func (m *Manager) continueProseQuestion(ctx context.Context, l *live, p agentProcess, final string, success bool) (bool, error) {
if !success || !l.implementer() {
return false, nil
}
bounce, err := m.takeBounce(ctx, l, final)
if err != nil || !bounce {
return false, err
}
m.mu.Lock()
l.lastText = ""
m.mu.Unlock()
go func() {
err := p.send(context.Background(), proseQuestionNudge, nil)
if err != nil {
slog.Error("send prose question nudge", "session", l.id, "err", err)
}
}()
return true, nil
}