This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

keep loop alive after nudge

+80 -10
+65
src/runner/loop.test.ts
··· 1 + import assert from "node:assert/strict" 2 + import test from "node:test" 3 + import { __loopTest } from "./loop.js" 4 + import type { LoopState } from "./types.js" 5 + 6 + function makeState(): LoopState { 7 + return { 8 + conversation: [], 9 + pendingInputs: [], 10 + tokenCount: 0, 11 + contextSize: 0, 12 + toolInFlight: false, 13 + memoryRecallCooldowns: {}, 14 + memoryRecallTurn: 0, 15 + } 16 + } 17 + 18 + test("applyDiscordSendNudge appends a follow-up user nudge for unsent Discord replies", () => { 19 + const state = makeState() 20 + const turnMessages = [ 21 + { 22 + role: "user", 23 + content: "[user/discord] [discord/dm] hey are you there", 24 + }, 25 + { 26 + role: "assistant", 27 + content: "yeah, i'm here", 28 + }, 29 + ] 30 + 31 + const nudged = __loopTest.applyDiscordSendNudge(state, turnMessages) 32 + 33 + assert.equal(nudged, true) 34 + assert.equal(state.conversation.length, 1) 35 + assert.match(String(state.conversation[0]?.content), /did not call discord_send/i) 36 + }) 37 + 38 + test("applyDiscordSendNudge does not fire when discord_send was already called", () => { 39 + const state = makeState() 40 + const turnMessages = [ 41 + { 42 + role: "user", 43 + content: "[user/discord] [discord/channel] can you reply", 44 + }, 45 + { 46 + role: "assistant", 47 + content: "sending now", 48 + tool_calls: [ 49 + { 50 + id: "call_123", 51 + type: "function", 52 + function: { 53 + name: "discord_send", 54 + arguments: "{\"channel_id\":\"1\",\"content\":\"sending now\"}", 55 + }, 56 + }, 57 + ], 58 + }, 59 + ] 60 + 61 + const nudged = __loopTest.applyDiscordSendNudge(state, turnMessages) 62 + 63 + assert.equal(nudged, false) 64 + assert.equal(state.conversation.length, 0) 65 + })
+15 -10
src/runner/loop.ts
··· 12 12 summarizeConversationViaLLM, 13 13 } from "./util.js" 14 14 import { addAssistantMessage, applyUsage, configuredSummaryProvider, emitThinking, fetchCompletion } from "./loop-completion.js" 15 - import { isFunctionToolCall } from "./loop-content.js" 15 + import { assistantContentText, isFunctionToolCall } from "./loop-content.js" 16 16 import { buildTurnSignature, hasIncomingUserMessage } from "./loop-signatures.js" 17 17 import { processToolCalls } from "./loop-tools.js" 18 18 import type { LoopHooks, LoopState } from "./types.js" ··· 76 76 * conversational text but did not call discord_send. Injects a system 77 77 * nudge so the next turn actually delivers the message. 78 78 */ 79 - function applyDiscordSendNudge(state: LoopState, turnMessages: OpenAI.Chat.ChatCompletionMessage[]): void { 79 + function applyDiscordSendNudge(state: LoopState, turnMessages: OpenAI.Chat.ChatCompletionMessage[]): boolean { 80 80 // Check if any incoming user message in this turn came from Discord 81 81 const hasDiscordInput = turnMessages.some( 82 82 (m) => m.role === "user" && typeof m.content === "string" && /\[discord\/(?:dm|batch|channel)\]/i.test(m.content), 83 83 ) 84 - if (!hasDiscordInput) return 84 + if (!hasDiscordInput) return false 85 85 86 86 // Check if the assistant called discord_send in this turn 87 87 const hasDiscordSend = turnMessages.some( ··· 90 90 Array.isArray(m.tool_calls) && 91 91 m.tool_calls.some((tc) => tc.type === "function" && tc.function.name === "discord_send"), 92 92 ) 93 - if (hasDiscordSend) return 93 + if (hasDiscordSend) return false 94 94 95 95 // Also check if a tool result from discord_send exists (edge case: tool result is separate message) 96 96 const hasDiscordSendResult = turnMessages.some( ··· 100 100 m.content.includes('"ok":true') && 101 101 m.content.includes("discord_send"), 102 102 ) 103 - if (hasDiscordSendResult) return 103 + if (hasDiscordSendResult) return false 104 104 105 105 // Find the assistant's text content in this turn 106 - const assistantText = turnMessages.find( 107 - (m) => m.role === "assistant" && typeof m.content === "string" && m.content.trim().length > 0, 108 - ) 109 - if (!assistantText || typeof assistantText !== "object") return 106 + const assistantText = turnMessages.find((m) => m.role === "assistant" && assistantContentText(m.content).length > 0) 107 + if (!assistantText) return false 110 108 111 109 // The assistant wrote something in response to a Discord message but 112 110 // never actually sent it. Nudge. 113 111 const nudge = `[system] you wrote a response to a Discord message but did not call discord_send. your message was not delivered. call discord_send now or explicitly decide not to reply.` 114 112 console.warn("[runner] discord_send nudge: assistant responded to Discord input without calling discord_send") 115 113 state.conversation.push({ role: "user", content: nudge }) 114 + return true 116 115 } 117 116 118 117 function applyContextNudge(state: LoopState): void { ··· 197 196 // a Discord message but forgets to call discord_send. This is a common 198 197 // hallucination pattern — the model writes a reply "in its head" and 199 198 // then calls wait/rest, leaving the Discord user in silence. 199 + let discordSendNudged = false 200 200 if (outcome !== CycleOutcome.Rest) { 201 - applyDiscordSendNudge(state, turnMessages) 201 + discordSendNudged = applyDiscordSendNudge(state, turnMessages) 202 202 } 203 203 204 204 if (interruptedByUserEvent || !turnSignature) { ··· 213 213 214 214 if (outcome === CycleOutcome.Rest) return "rest" 215 215 if (outcome === CycleOutcome.NoTools) { 216 + if (discordSendNudged) continue 216 217 await hooks.saveSession() 217 218 return "silent_complete" 218 219 } ··· 234 235 await hooks.saveSession() 235 236 } 236 237 } 238 + 239 + export const __loopTest = { 240 + applyDiscordSendNudge, 241 + }