mirror of
https://github.com/memohai/Memoh.git
synced 2026-04-25 07:00:48 +09:00
164 lines
4.6 KiB
Go
164 lines
4.6 KiB
Go
package agent
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"net/http"
|
|
"strings"
|
|
|
|
sdk "github.com/memohai/twilight-ai/sdk"
|
|
|
|
"github.com/memohai/memoh/internal/agent/tools"
|
|
"github.com/memohai/memoh/internal/models"
|
|
)
|
|
|
|
// SpawnAdapter wraps *Agent to satisfy tools.SpawnAgent without creating
|
|
// an import cycle (tools -> agent).
|
|
type SpawnAdapter struct {
|
|
agent *Agent
|
|
}
|
|
|
|
// NewSpawnAdapter creates a SpawnAdapter from the given Agent.
|
|
func NewSpawnAdapter(a *Agent) *SpawnAdapter {
|
|
return &SpawnAdapter{agent: a}
|
|
}
|
|
|
|
func (s *SpawnAdapter) Generate(ctx context.Context, cfg tools.SpawnRunConfig) (*tools.SpawnResult, error) {
|
|
messages := cfg.Messages
|
|
if cfg.Query != "" {
|
|
messages = append(messages, sdk.Message{
|
|
Role: sdk.MessageRoleUser,
|
|
Content: []sdk.MessagePart{sdk.TextPart{Text: cfg.Query}},
|
|
})
|
|
}
|
|
|
|
rc := RunConfig{
|
|
Model: cfg.Model,
|
|
System: cfg.System,
|
|
Query: cfg.Query,
|
|
SessionType: cfg.SessionType,
|
|
Messages: messages,
|
|
ReasoningEffort: cfg.ReasoningEffort,
|
|
SupportsToolCall: true,
|
|
Identity: SessionContext{
|
|
BotID: cfg.Identity.BotID,
|
|
ChatID: cfg.Identity.ChatID,
|
|
SessionID: cfg.Identity.SessionID,
|
|
ChannelIdentityID: cfg.Identity.ChannelIdentityID,
|
|
CurrentPlatform: cfg.Identity.CurrentPlatform,
|
|
SessionToken: cfg.Identity.SessionToken,
|
|
IsSubagent: cfg.Identity.IsSubagent,
|
|
},
|
|
LoopDetection: LoopDetectionConfig{
|
|
Enabled: cfg.LoopDetection.Enabled,
|
|
},
|
|
}
|
|
|
|
result, err := s.agent.Generate(ctx, rc)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &tools.SpawnResult{
|
|
Messages: result.Messages,
|
|
Text: result.Text,
|
|
Usage: result.Usage,
|
|
}, nil
|
|
}
|
|
|
|
// GenerateWithWatchdog runs the agent in streaming mode, touching the
|
|
// provided touchFn on every stream event (token, tool progress, etc.).
|
|
// It collects the full result and returns it in the same shape as Generate.
|
|
// This enables activity-based watchdog monitoring for subagent execution.
|
|
func (s *SpawnAdapter) GenerateWithWatchdog(ctx context.Context, cfg tools.SpawnRunConfig, touchFn func()) (*tools.SpawnResult, error) {
|
|
messages := cfg.Messages
|
|
if cfg.Query != "" {
|
|
messages = append(messages, sdk.Message{
|
|
Role: sdk.MessageRoleUser,
|
|
Content: []sdk.MessagePart{sdk.TextPart{Text: cfg.Query}},
|
|
})
|
|
}
|
|
|
|
rc := RunConfig{
|
|
Model: cfg.Model,
|
|
System: cfg.System,
|
|
Query: cfg.Query,
|
|
SessionType: cfg.SessionType,
|
|
Messages: messages,
|
|
ReasoningEffort: cfg.ReasoningEffort,
|
|
SupportsToolCall: true,
|
|
Identity: SessionContext{
|
|
BotID: cfg.Identity.BotID,
|
|
ChatID: cfg.Identity.ChatID,
|
|
SessionID: cfg.Identity.SessionID,
|
|
ChannelIdentityID: cfg.Identity.ChannelIdentityID,
|
|
CurrentPlatform: cfg.Identity.CurrentPlatform,
|
|
SessionToken: cfg.Identity.SessionToken,
|
|
IsSubagent: cfg.Identity.IsSubagent,
|
|
},
|
|
LoopDetection: LoopDetectionConfig{
|
|
Enabled: cfg.LoopDetection.Enabled,
|
|
},
|
|
}
|
|
|
|
// Use Stream instead of Generate to get per-token/per-tool activity signals.
|
|
eventCh := s.agent.Stream(ctx, rc)
|
|
|
|
var allText strings.Builder
|
|
var finalMessages []sdk.Message
|
|
var totalUsage sdk.Usage
|
|
|
|
for evt := range eventCh {
|
|
// Touch the watchdog on every event — this is the activity signal.
|
|
touchFn()
|
|
|
|
switch evt.Type {
|
|
case EventTextDelta:
|
|
allText.WriteString(evt.Delta)
|
|
case EventAgentEnd, EventAgentAbort:
|
|
if evt.Messages != nil {
|
|
_ = json.Unmarshal(evt.Messages, &finalMessages)
|
|
}
|
|
if evt.Usage != nil {
|
|
_ = json.Unmarshal(evt.Usage, &totalUsage)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check if context was cancelled (watchdog fired or parent cancelled).
|
|
if ctx.Err() != nil {
|
|
if cause := context.Cause(ctx); cause != nil {
|
|
return nil, cause
|
|
}
|
|
return nil, ctx.Err()
|
|
}
|
|
|
|
return &tools.SpawnResult{
|
|
Messages: finalMessages,
|
|
Text: allText.String(),
|
|
Usage: &totalUsage,
|
|
}, nil
|
|
}
|
|
|
|
// SpawnSystemPrompt returns the system prompt for a given session type.
|
|
func SpawnSystemPrompt(sessionType string) string {
|
|
return GenerateSystemPrompt(SystemPromptParams{
|
|
SessionType: sessionType,
|
|
})
|
|
}
|
|
|
|
// SpawnModelCreatorFunc returns a tools.ModelCreator backed by the shared SDK model factory.
|
|
// This keeps subagent model creation aligned with the shared SDK model factory.
|
|
func SpawnModelCreatorFunc() tools.ModelCreator {
|
|
return func(modelID, clientType, apiKey, codexAccountID, baseURL string, httpClient *http.Client) *sdk.Model {
|
|
return models.NewSDKChatModel(models.SDKModelConfig{
|
|
ModelID: modelID,
|
|
ClientType: clientType,
|
|
APIKey: apiKey,
|
|
CodexAccountID: codexAccountID,
|
|
BaseURL: baseURL,
|
|
HTTPClient: httpClient,
|
|
})
|
|
}
|
|
}
|