feat(agent): add background task execution and notifications (#365)

This commit is contained in:
EYHN
2026-04-13 20:28:42 +08:00
committed by GitHub
parent df8fbd8859
commit c4114227e5
18 changed files with 2934 additions and 31 deletions
+66
View File
@@ -11,6 +11,7 @@ import (
sdk "github.com/memohai/twilight-ai/sdk"
"github.com/memohai/memoh/internal/agent/background"
"github.com/memohai/memoh/internal/agent/tools"
"github.com/memohai/memoh/internal/models"
"github.com/memohai/memoh/internal/workspace/bridge"
@@ -169,6 +170,23 @@ func (a *Agent) runStream(ctx context.Context, cfg RunConfig, ch chan<- StreamEv
}
}
// Drain background task notifications at step boundaries.
// Each notification is injected as a user message so the model
// discovers completed background work naturally.
if cfg.BackgroundManager != nil {
basePrepare := prepareStep
baseSystem := cfg.System // capture original system prompt to avoid accumulation
prepareStep = func(p *sdk.GenerateParams) *sdk.GenerateParams {
if basePrepare != nil {
if override := basePrepare(p); override != nil {
p = override
}
}
p = drainBackgroundNotifications(p, cfg.BackgroundManager, baseSystem, cfg.Identity.BotID, cfg.Identity.SessionID, a.logger)
return p
}
}
opts := a.buildGenerateOptions(cfg, sdkTools, prepareStep)
retryCfg := cfg.Retry
@@ -452,6 +470,22 @@ func (a *Agent) runGenerate(ctx context.Context, cfg RunConfig) (*GenerateResult
if readMediaState != nil {
prepareStep = readMediaState.prepareStep
}
// Drain background task notifications at step boundaries (non-streaming).
if cfg.BackgroundManager != nil {
basePrepare := prepareStep
baseSystem := cfg.System
prepareStep = func(p *sdk.GenerateParams) *sdk.GenerateParams {
if basePrepare != nil {
if override := basePrepare(p); override != nil {
p = override
}
}
p = drainBackgroundNotifications(p, cfg.BackgroundManager, baseSystem, cfg.Identity.BotID, cfg.Identity.SessionID, a.logger)
return p
}
}
opts := a.buildGenerateOptions(cfg, sdkTools, prepareStep)
opts = append(opts,
sdk.WithOnStep(func(step *sdk.StepResult) *sdk.GenerateParams {
@@ -635,6 +669,38 @@ func toolStreamEventToAgentEvent(evt tools.ToolStreamEvent) StreamEvent {
}
}
// drainBackgroundNotifications non-blockingly drains pending background task
// notifications for the given bot+session and injects them as user messages
// into the next LLM step at step boundaries.
func drainBackgroundNotifications(
p *sdk.GenerateParams,
mgr *background.Manager,
baseSystem string,
botID, sessionID string,
logger *slog.Logger,
) *sdk.GenerateParams {
// Inject running tasks summary into system prompt so the model
// knows about ongoing background work even after compaction.
// Always start from baseSystem to avoid accumulating summaries across steps.
if summary := mgr.RunningTasksSummary(botID, sessionID); summary != "" {
p.System = baseSystem + "\n\n" + summary
} else {
p.System = baseSystem
}
notifications := mgr.DrainNotifications(botID, sessionID)
for _, n := range notifications {
p.Messages = append(p.Messages, sdk.UserMessage(n.MessageText()))
logger.Info("injected background task notification",
slog.String("task_id", n.TaskID),
slog.String("status", string(n.Status)),
slog.Bool("stalled", n.Stalled),
slog.String("bot_id", botID),
)
}
return p
}
func wrapToolsWithLoopGuard(tools []sdk.Tool, guard *ToolLoopGuard, abortCallIDs map[string]struct{}) []sdk.Tool {
wrapped := make([]sdk.Tool, len(tools))
for i, tool := range tools {