mirror of
https://github.com/sipeed/picoclaw.git
synced 2026-06-12 18:08:54 +00:00
refactor(context): carry route and scope through runtime
This commit is contained in:
@@ -10,6 +10,8 @@ import (
|
||||
"github.com/sipeed/picoclaw/pkg/bus"
|
||||
"github.com/sipeed/picoclaw/pkg/config"
|
||||
"github.com/sipeed/picoclaw/pkg/providers"
|
||||
"github.com/sipeed/picoclaw/pkg/routing"
|
||||
"github.com/sipeed/picoclaw/pkg/session"
|
||||
"github.com/sipeed/picoclaw/pkg/tools"
|
||||
)
|
||||
|
||||
@@ -142,6 +144,25 @@ func TestAgentLoop_EmitsMinimalTurnEvents(t *testing.T) {
|
||||
ChatType: "direct",
|
||||
SenderID: "tester",
|
||||
},
|
||||
RouteResult: &routing.ResolvedRoute{
|
||||
AgentID: "main",
|
||||
Channel: "cli",
|
||||
AccountID: routing.DefaultAccountID,
|
||||
SessionPolicy: routing.SessionPolicy{
|
||||
DMScope: routing.DMScopePerPeer,
|
||||
},
|
||||
MatchedBy: "default",
|
||||
},
|
||||
SessionScope: &session.SessionScope{
|
||||
Version: session.ScopeVersionV1,
|
||||
AgentID: "main",
|
||||
Channel: "cli",
|
||||
Account: routing.DefaultAccountID,
|
||||
Dimensions: []string{"sender"},
|
||||
Values: map[string]string{
|
||||
"sender": "tester",
|
||||
},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("runAgentLoop failed: %v", err)
|
||||
@@ -182,11 +203,17 @@ func TestAgentLoop_EmitsMinimalTurnEvents(t *testing.T) {
|
||||
if evt.Meta.SessionKey != "session-1" {
|
||||
t.Fatalf("event %d has session key %q, want session-1", i, evt.Meta.SessionKey)
|
||||
}
|
||||
if evt.Meta.Context == nil || evt.Meta.Context.Inbound == nil {
|
||||
if evt.Context == nil || evt.Context.Inbound == nil {
|
||||
t.Fatalf("event %d missing inbound turn context", i)
|
||||
}
|
||||
if evt.Meta.Context.Inbound.Channel != "cli" || evt.Meta.Context.Inbound.SenderID != "tester" {
|
||||
t.Fatalf("event %d inbound context = %+v", i, evt.Meta.Context.Inbound)
|
||||
if evt.Context.Inbound.Channel != "cli" || evt.Context.Inbound.SenderID != "tester" {
|
||||
t.Fatalf("event %d inbound context = %+v", i, evt.Context.Inbound)
|
||||
}
|
||||
if evt.Context.Route == nil || evt.Context.Route.AgentID != "main" {
|
||||
t.Fatalf("event %d missing route context: %+v", i, evt.Context.Route)
|
||||
}
|
||||
if evt.Context.Scope == nil || evt.Context.Scope.Values["sender"] != "tester" {
|
||||
t.Fatalf("event %d missing session scope: %+v", i, evt.Context.Scope)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+2
-1
@@ -86,6 +86,7 @@ type Event struct {
|
||||
Kind EventKind
|
||||
Time time.Time
|
||||
Meta EventMeta
|
||||
Context *TurnContext
|
||||
Payload any
|
||||
}
|
||||
|
||||
@@ -98,7 +99,7 @@ type EventMeta struct {
|
||||
Iteration int
|
||||
TracePath string
|
||||
Source string
|
||||
Context *TurnContext `json:"context,omitempty"`
|
||||
turnContext *TurnContext
|
||||
}
|
||||
|
||||
// TurnEndStatus describes the terminal state of a turn.
|
||||
|
||||
@@ -89,6 +89,7 @@ type ToolApprover interface {
|
||||
|
||||
type LLMHookRequest struct {
|
||||
Meta EventMeta `json:"meta"`
|
||||
Context *TurnContext `json:"context,omitempty"`
|
||||
Model string `json:"model"`
|
||||
Messages []providers.Message `json:"messages,omitempty"`
|
||||
Tools []providers.ToolDefinition `json:"tools,omitempty"`
|
||||
@@ -104,6 +105,7 @@ func (r *LLMHookRequest) Clone() *LLMHookRequest {
|
||||
}
|
||||
cloned := *r
|
||||
cloned.Meta = cloneEventMeta(r.Meta)
|
||||
cloned.Context = cloneTurnContext(r.Context)
|
||||
cloned.Messages = cloneProviderMessages(r.Messages)
|
||||
cloned.Tools = cloneToolDefinitions(r.Tools)
|
||||
cloned.Options = cloneStringAnyMap(r.Options)
|
||||
@@ -112,6 +114,7 @@ func (r *LLMHookRequest) Clone() *LLMHookRequest {
|
||||
|
||||
type LLMHookResponse struct {
|
||||
Meta EventMeta `json:"meta"`
|
||||
Context *TurnContext `json:"context,omitempty"`
|
||||
Model string `json:"model"`
|
||||
Response *providers.LLMResponse `json:"response,omitempty"`
|
||||
Channel string `json:"channel,omitempty"`
|
||||
@@ -124,12 +127,14 @@ func (r *LLMHookResponse) Clone() *LLMHookResponse {
|
||||
}
|
||||
cloned := *r
|
||||
cloned.Meta = cloneEventMeta(r.Meta)
|
||||
cloned.Context = cloneTurnContext(r.Context)
|
||||
cloned.Response = cloneLLMResponse(r.Response)
|
||||
return &cloned
|
||||
}
|
||||
|
||||
type ToolCallHookRequest struct {
|
||||
Meta EventMeta `json:"meta"`
|
||||
Context *TurnContext `json:"context,omitempty"`
|
||||
Tool string `json:"tool"`
|
||||
Arguments map[string]any `json:"arguments,omitempty"`
|
||||
Channel string `json:"channel,omitempty"`
|
||||
@@ -142,12 +147,14 @@ func (r *ToolCallHookRequest) Clone() *ToolCallHookRequest {
|
||||
}
|
||||
cloned := *r
|
||||
cloned.Meta = cloneEventMeta(r.Meta)
|
||||
cloned.Context = cloneTurnContext(r.Context)
|
||||
cloned.Arguments = cloneStringAnyMap(r.Arguments)
|
||||
return &cloned
|
||||
}
|
||||
|
||||
type ToolApprovalRequest struct {
|
||||
Meta EventMeta `json:"meta"`
|
||||
Context *TurnContext `json:"context,omitempty"`
|
||||
Tool string `json:"tool"`
|
||||
Arguments map[string]any `json:"arguments,omitempty"`
|
||||
Channel string `json:"channel,omitempty"`
|
||||
@@ -160,12 +167,14 @@ func (r *ToolApprovalRequest) Clone() *ToolApprovalRequest {
|
||||
}
|
||||
cloned := *r
|
||||
cloned.Meta = cloneEventMeta(r.Meta)
|
||||
cloned.Context = cloneTurnContext(r.Context)
|
||||
cloned.Arguments = cloneStringAnyMap(r.Arguments)
|
||||
return &cloned
|
||||
}
|
||||
|
||||
type ToolResultHookResponse struct {
|
||||
Meta EventMeta `json:"meta"`
|
||||
Context *TurnContext `json:"context,omitempty"`
|
||||
Tool string `json:"tool"`
|
||||
Arguments map[string]any `json:"arguments,omitempty"`
|
||||
Result *tools.ToolResult `json:"result,omitempty"`
|
||||
@@ -180,6 +189,7 @@ func (r *ToolResultHookResponse) Clone() *ToolResultHookResponse {
|
||||
}
|
||||
cloned := *r
|
||||
cloned.Meta = cloneEventMeta(r.Meta)
|
||||
cloned.Context = cloneTurnContext(r.Context)
|
||||
cloned.Arguments = cloneStringAnyMap(r.Arguments)
|
||||
cloned.Result = cloneToolResult(r.Result)
|
||||
return &cloned
|
||||
|
||||
+33
-3
@@ -10,6 +10,8 @@ import (
|
||||
"github.com/sipeed/picoclaw/pkg/bus"
|
||||
"github.com/sipeed/picoclaw/pkg/config"
|
||||
"github.com/sipeed/picoclaw/pkg/providers"
|
||||
"github.com/sipeed/picoclaw/pkg/routing"
|
||||
"github.com/sipeed/picoclaw/pkg/session"
|
||||
"github.com/sipeed/picoclaw/pkg/tools"
|
||||
)
|
||||
|
||||
@@ -124,8 +126,8 @@ func (h *llmObserverHook) BeforeLLM(
|
||||
ctx context.Context,
|
||||
req *LLMHookRequest,
|
||||
) (*LLMHookRequest, HookDecision, error) {
|
||||
if req.Meta.Context != nil {
|
||||
h.lastInbound = cloneInboundContext(req.Meta.Context.Inbound)
|
||||
if req.Context != nil {
|
||||
h.lastInbound = cloneInboundContext(req.Context.Inbound)
|
||||
}
|
||||
next := req.Clone()
|
||||
next.Model = "hook-model"
|
||||
@@ -165,6 +167,25 @@ func TestAgentLoop_Hooks_ObserverAndLLMInterceptor(t *testing.T) {
|
||||
ChatType: "direct",
|
||||
SenderID: "hook-user",
|
||||
},
|
||||
RouteResult: &routing.ResolvedRoute{
|
||||
AgentID: "main",
|
||||
Channel: "cli",
|
||||
AccountID: routing.DefaultAccountID,
|
||||
SessionPolicy: routing.SessionPolicy{
|
||||
DMScope: routing.DMScopePerPeer,
|
||||
},
|
||||
MatchedBy: "default",
|
||||
},
|
||||
SessionScope: &session.SessionScope{
|
||||
Version: session.ScopeVersionV1,
|
||||
AgentID: "main",
|
||||
Channel: "cli",
|
||||
Account: routing.DefaultAccountID,
|
||||
Dimensions: []string{"sender"},
|
||||
Values: map[string]string{
|
||||
"sender": "hook-user",
|
||||
},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("runAgentLoop failed: %v", err)
|
||||
@@ -185,15 +206,24 @@ func TestAgentLoop_Hooks_ObserverAndLLMInterceptor(t *testing.T) {
|
||||
if hook.lastInbound.Channel != "cli" || hook.lastInbound.SenderID != "hook-user" {
|
||||
t.Fatalf("hook inbound context = %+v", hook.lastInbound)
|
||||
}
|
||||
if hook.lastInbound != nil && hook.lastInbound.ChatID != "direct" {
|
||||
t.Fatalf("hook inbound chat ID = %q, want direct", hook.lastInbound.ChatID)
|
||||
}
|
||||
|
||||
select {
|
||||
case evt := <-hook.eventCh:
|
||||
if evt.Kind != EventKindTurnEnd {
|
||||
t.Fatalf("expected turn end event, got %v", evt.Kind)
|
||||
}
|
||||
if evt.Meta.Context == nil || evt.Meta.Context.Inbound == nil {
|
||||
if evt.Context == nil || evt.Context.Inbound == nil {
|
||||
t.Fatal("expected observer event to carry inbound context")
|
||||
}
|
||||
if evt.Context.Route == nil || evt.Context.Route.AgentID != "main" {
|
||||
t.Fatalf("expected observer event to carry route context, got %+v", evt.Context.Route)
|
||||
}
|
||||
if evt.Context.Scope == nil || evt.Context.Scope.Values["sender"] != "hook-user" {
|
||||
t.Fatalf("expected observer event to carry session scope, got %+v", evt.Context.Scope)
|
||||
}
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Fatal("timed out waiting for hook observer event")
|
||||
}
|
||||
|
||||
+95
-48
@@ -73,25 +73,27 @@ type AgentLoop struct {
|
||||
|
||||
// processOptions configures how a message is processed
|
||||
type processOptions struct {
|
||||
SessionKey string // Session identifier for history/context
|
||||
Channel string // Target channel for tool execution
|
||||
ChatID string // Target chat ID for tool execution
|
||||
MessageID string // Current inbound platform message ID
|
||||
ReplyToMessageID string // Current inbound reply target message ID
|
||||
SenderID string // Current sender ID for dynamic context
|
||||
SenderDisplayName string // Current sender display name for dynamic context
|
||||
UserMessage string // User message content (may include prefix)
|
||||
ForcedSkills []string // Skills explicitly requested for this message
|
||||
SystemPromptOverride string // Override the default system prompt (Used by SubTurns)
|
||||
Media []string // media:// refs from inbound message
|
||||
InitialSteeringMessages []providers.Message // Steering messages from refactor/agent
|
||||
DefaultResponse string // Response when LLM returns empty
|
||||
EnableSummary bool // Whether to trigger summarization
|
||||
SendResponse bool // Whether to send response via bus
|
||||
SuppressToolFeedback bool // Whether to suppress inline tool feedback messages
|
||||
NoHistory bool // If true, don't load session history (for heartbeat)
|
||||
SkipInitialSteeringPoll bool // If true, skip the steering poll at loop start (used by Continue)
|
||||
InboundContext *bus.InboundContext // Normalized inbound facts for events/hooks
|
||||
SessionKey string // Session identifier for history/context
|
||||
Channel string // Target channel for tool execution
|
||||
ChatID string // Target chat ID for tool execution
|
||||
MessageID string // Current inbound platform message ID
|
||||
ReplyToMessageID string // Current inbound reply target message ID
|
||||
SenderID string // Current sender ID for dynamic context
|
||||
SenderDisplayName string // Current sender display name for dynamic context
|
||||
UserMessage string // User message content (may include prefix)
|
||||
ForcedSkills []string // Skills explicitly requested for this message
|
||||
SystemPromptOverride string // Override the default system prompt (Used by SubTurns)
|
||||
Media []string // media:// refs from inbound message
|
||||
InitialSteeringMessages []providers.Message // Steering messages from refactor/agent
|
||||
DefaultResponse string // Response when LLM returns empty
|
||||
EnableSummary bool // Whether to trigger summarization
|
||||
SendResponse bool // Whether to send response via bus
|
||||
SuppressToolFeedback bool // Whether to suppress inline tool feedback messages
|
||||
NoHistory bool // If true, don't load session history (for heartbeat)
|
||||
SkipInitialSteeringPoll bool // If true, skip the steering poll at loop start (used by Continue)
|
||||
InboundContext *bus.InboundContext // Normalized inbound facts for events/hooks
|
||||
RouteResult *routing.ResolvedRoute // Route decision snapshot for events/hooks
|
||||
SessionScope *session.SessionScope // Session scope snapshot for events/hooks
|
||||
}
|
||||
|
||||
type continuationTarget struct {
|
||||
@@ -705,6 +707,45 @@ func (al *AgentLoop) Close() {
|
||||
}
|
||||
}
|
||||
|
||||
func outboundContextFromInbound(
|
||||
inbound *bus.InboundContext,
|
||||
channel, chatID, replyToMessageID string,
|
||||
) bus.InboundContext {
|
||||
if inbound == nil {
|
||||
return bus.ContextFromLegacyOutbound(bus.OutboundMessage{
|
||||
Channel: channel,
|
||||
ChatID: chatID,
|
||||
ReplyToMessageID: replyToMessageID,
|
||||
})
|
||||
}
|
||||
|
||||
outboundCtx := *cloneInboundContext(inbound)
|
||||
if outboundCtx.Channel == "" {
|
||||
outboundCtx.Channel = channel
|
||||
}
|
||||
if outboundCtx.ChatID == "" {
|
||||
outboundCtx.ChatID = chatID
|
||||
}
|
||||
if outboundCtx.ReplyToMessageID == "" {
|
||||
outboundCtx.ReplyToMessageID = replyToMessageID
|
||||
}
|
||||
return outboundCtx
|
||||
}
|
||||
|
||||
func outboundMessageForTurn(ts *turnState, content string) bus.OutboundMessage {
|
||||
return bus.OutboundMessage{
|
||||
Channel: ts.channel,
|
||||
ChatID: ts.chatID,
|
||||
Context: outboundContextFromInbound(
|
||||
ts.opts.InboundContext,
|
||||
ts.channel,
|
||||
ts.chatID,
|
||||
ts.opts.ReplyToMessageID,
|
||||
),
|
||||
Content: content,
|
||||
}
|
||||
}
|
||||
|
||||
// MountHook registers an in-process hook on the agent loop.
|
||||
func (al *AgentLoop) MountHook(reg HookRegistration) error {
|
||||
if al == nil || al.hooks == nil {
|
||||
@@ -766,20 +807,22 @@ func (al *AgentLoop) newTurnEventScope(agentID, sessionKey string, turnCtx *Turn
|
||||
|
||||
func (ts turnEventScope) meta(iteration int, source, tracePath string) EventMeta {
|
||||
return EventMeta{
|
||||
AgentID: ts.agentID,
|
||||
TurnID: ts.turnID,
|
||||
SessionKey: ts.sessionKey,
|
||||
Iteration: iteration,
|
||||
Source: source,
|
||||
TracePath: tracePath,
|
||||
Context: cloneTurnContext(ts.context),
|
||||
AgentID: ts.agentID,
|
||||
TurnID: ts.turnID,
|
||||
SessionKey: ts.sessionKey,
|
||||
Iteration: iteration,
|
||||
Source: source,
|
||||
TracePath: tracePath,
|
||||
turnContext: cloneTurnContext(ts.context),
|
||||
}
|
||||
}
|
||||
|
||||
func (al *AgentLoop) emitEvent(kind EventKind, meta EventMeta, payload any) {
|
||||
clonedMeta := cloneEventMeta(meta)
|
||||
evt := Event{
|
||||
Kind: kind,
|
||||
Meta: cloneEventMeta(meta),
|
||||
Meta: clonedMeta,
|
||||
Context: cloneTurnContext(clonedMeta.turnContext),
|
||||
Payload: payload,
|
||||
}
|
||||
|
||||
@@ -1361,6 +1404,8 @@ func (al *AgentLoop) processMessage(ctx context.Context, msg bus.InboundMessage)
|
||||
EnableSummary: true,
|
||||
SendResponse: false,
|
||||
InboundContext: cloneInboundContext(&msg.Context),
|
||||
RouteResult: cloneResolvedRoute(&route),
|
||||
SessionScope: session.CloneScope(&allocation.Scope),
|
||||
}
|
||||
|
||||
// context-dependent commands check their own Runtime fields and report
|
||||
@@ -1540,7 +1585,11 @@ func (al *AgentLoop) runAgentLoop(
|
||||
}
|
||||
}
|
||||
|
||||
turnScope := al.newTurnEventScope(agent.ID, opts.SessionKey, newTurnContext(opts.InboundContext))
|
||||
turnScope := al.newTurnEventScope(
|
||||
agent.ID,
|
||||
opts.SessionKey,
|
||||
newTurnContext(opts.InboundContext, opts.RouteResult, opts.SessionScope),
|
||||
)
|
||||
ts := newTurnState(agent, opts, turnScope)
|
||||
result, err := al.runTurn(ctx, ts)
|
||||
if err != nil {
|
||||
@@ -1564,6 +1613,12 @@ func (al *AgentLoop) runAgentLoop(
|
||||
al.bus.PublishOutbound(ctx, bus.OutboundMessage{
|
||||
Channel: opts.Channel,
|
||||
ChatID: opts.ChatID,
|
||||
Context: outboundContextFromInbound(
|
||||
opts.InboundContext,
|
||||
opts.Channel,
|
||||
opts.ChatID,
|
||||
opts.ReplyToMessageID,
|
||||
),
|
||||
Content: result.finalContent,
|
||||
})
|
||||
}
|
||||
@@ -1897,6 +1952,7 @@ turnLoop:
|
||||
if al.hooks != nil {
|
||||
llmReq, decision := al.hooks.BeforeLLM(turnCtx, &LLMHookRequest{
|
||||
Meta: ts.eventMeta("runTurn", "turn.llm.request"),
|
||||
Context: cloneTurnContext(ts.turnCtx),
|
||||
Model: llmModel,
|
||||
Messages: callMessages,
|
||||
Tools: providerToolDefs,
|
||||
@@ -2069,11 +2125,10 @@ turnLoop:
|
||||
)
|
||||
|
||||
if retry == 0 && !constants.IsInternalChannel(ts.channel) {
|
||||
al.bus.PublishOutbound(ctx, bus.OutboundMessage{
|
||||
Channel: ts.channel,
|
||||
ChatID: ts.chatID,
|
||||
Content: "Context window exceeded. Compressing history and retrying...",
|
||||
})
|
||||
al.bus.PublishOutbound(ctx, outboundMessageForTurn(
|
||||
ts,
|
||||
"Context window exceeded. Compressing history and retrying...",
|
||||
))
|
||||
}
|
||||
|
||||
if compression, ok := al.forceCompression(ts.agent, ts.sessionKey); ok {
|
||||
@@ -2128,6 +2183,7 @@ turnLoop:
|
||||
if al.hooks != nil {
|
||||
llmResp, decision := al.hooks.AfterLLM(turnCtx, &LLMHookResponse{
|
||||
Meta: ts.eventMeta("runTurn", "turn.llm.response"),
|
||||
Context: cloneTurnContext(ts.turnCtx),
|
||||
Model: llmModel,
|
||||
Response: response,
|
||||
Channel: ts.channel,
|
||||
@@ -2280,6 +2336,7 @@ turnLoop:
|
||||
if al.hooks != nil {
|
||||
toolReq, decision := al.hooks.BeforeTool(turnCtx, &ToolCallHookRequest{
|
||||
Meta: ts.eventMeta("runTurn", "turn.tool.before"),
|
||||
Context: cloneTurnContext(ts.turnCtx),
|
||||
Tool: toolName,
|
||||
Arguments: toolArgs,
|
||||
Channel: ts.channel,
|
||||
@@ -2326,6 +2383,7 @@ turnLoop:
|
||||
if al.hooks != nil {
|
||||
approval := al.hooks.ApproveTool(turnCtx, &ToolApprovalRequest{
|
||||
Meta: ts.eventMeta("runTurn", "turn.tool.approve"),
|
||||
Context: cloneTurnContext(ts.turnCtx),
|
||||
Tool: toolName,
|
||||
Arguments: toolArgs,
|
||||
Channel: ts.channel,
|
||||
@@ -2383,11 +2441,7 @@ turnLoop:
|
||||
)
|
||||
feedbackMsg := fmt.Sprintf("\U0001f527 `%s`\n```\n%s\n```", tc.Name, feedbackPreview)
|
||||
fbCtx, fbCancel := context.WithTimeout(turnCtx, 3*time.Second)
|
||||
_ = al.bus.PublishOutbound(fbCtx, bus.OutboundMessage{
|
||||
Channel: ts.channel,
|
||||
ChatID: ts.chatID,
|
||||
Content: feedbackMsg,
|
||||
})
|
||||
_ = al.bus.PublishOutbound(fbCtx, outboundMessageForTurn(ts, feedbackMsg))
|
||||
fbCancel()
|
||||
}
|
||||
|
||||
@@ -2400,11 +2454,7 @@ turnLoop:
|
||||
if !result.Silent && result.ForUser != "" {
|
||||
outCtx, outCancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer outCancel()
|
||||
_ = al.bus.PublishOutbound(outCtx, bus.OutboundMessage{
|
||||
Channel: ts.channel,
|
||||
ChatID: ts.chatID,
|
||||
Content: result.ForUser,
|
||||
})
|
||||
_ = al.bus.PublishOutbound(outCtx, outboundMessageForTurn(ts, result.ForUser))
|
||||
}
|
||||
|
||||
// Determine content for the agent loop (ForLLM or error).
|
||||
@@ -2469,6 +2519,7 @@ turnLoop:
|
||||
if al.hooks != nil {
|
||||
toolResp, decision := al.hooks.AfterTool(turnCtx, &ToolResultHookResponse{
|
||||
Meta: ts.eventMeta("runTurn", "turn.tool.after"),
|
||||
Context: cloneTurnContext(ts.turnCtx),
|
||||
Tool: toolName,
|
||||
Arguments: toolArgs,
|
||||
Result: toolResult,
|
||||
@@ -2545,11 +2596,7 @@ turnLoop:
|
||||
}
|
||||
|
||||
if !toolResult.Silent && toolResult.ForUser != "" && ts.opts.SendResponse {
|
||||
al.bus.PublishOutbound(ctx, bus.OutboundMessage{
|
||||
Channel: ts.channel,
|
||||
ChatID: ts.chatID,
|
||||
Content: toolResult.ForUser,
|
||||
})
|
||||
al.bus.PublishOutbound(ctx, outboundMessageForTurn(ts, toolResult.ForUser))
|
||||
logger.DebugCF("agent", "Sent tool result to user",
|
||||
map[string]any{
|
||||
"tool": toolName,
|
||||
|
||||
@@ -370,7 +370,11 @@ func spawnSubTurn(
|
||||
}
|
||||
|
||||
// Create event scope for the child turn
|
||||
scope := al.newTurnEventScope(agent.ID, childID, newTurnContext(opts.InboundContext))
|
||||
scope := al.newTurnEventScope(
|
||||
agent.ID,
|
||||
childID,
|
||||
newTurnContext(opts.InboundContext, opts.RouteResult, opts.SessionScope),
|
||||
)
|
||||
|
||||
// Create child turnState using the new API
|
||||
childTS := newTurnState(&agent, opts, scope)
|
||||
|
||||
+7
-7
@@ -303,13 +303,13 @@ func (ts *turnState) hardAbortRequested() bool {
|
||||
func (ts *turnState) eventMeta(source, tracePath string) EventMeta {
|
||||
snap := ts.snapshot()
|
||||
return EventMeta{
|
||||
AgentID: snap.AgentID,
|
||||
TurnID: snap.TurnID,
|
||||
SessionKey: snap.SessionKey,
|
||||
Iteration: snap.Iteration,
|
||||
Source: source,
|
||||
TracePath: tracePath,
|
||||
Context: cloneTurnContext(ts.turnCtx),
|
||||
AgentID: snap.AgentID,
|
||||
TurnID: snap.TurnID,
|
||||
SessionKey: snap.SessionKey,
|
||||
Iteration: snap.Iteration,
|
||||
Source: source,
|
||||
TracePath: tracePath,
|
||||
turnContext: cloneTurnContext(ts.turnCtx),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,19 +1,31 @@
|
||||
package agent
|
||||
|
||||
import "github.com/sipeed/picoclaw/pkg/bus"
|
||||
import (
|
||||
"github.com/sipeed/picoclaw/pkg/bus"
|
||||
"github.com/sipeed/picoclaw/pkg/routing"
|
||||
"github.com/sipeed/picoclaw/pkg/session"
|
||||
)
|
||||
|
||||
// TurnContext carries normalized turn-scoped facts that can be shared across
|
||||
// events, hooks, and other runtime observers without re-parsing legacy fields.
|
||||
type TurnContext struct {
|
||||
Inbound *bus.InboundContext `json:"inbound,omitempty"`
|
||||
Inbound *bus.InboundContext `json:"inbound,omitempty"`
|
||||
Route *routing.ResolvedRoute `json:"route,omitempty"`
|
||||
Scope *session.SessionScope `json:"scope,omitempty"`
|
||||
}
|
||||
|
||||
func newTurnContext(inbound *bus.InboundContext) *TurnContext {
|
||||
if inbound == nil {
|
||||
func newTurnContext(
|
||||
inbound *bus.InboundContext,
|
||||
route *routing.ResolvedRoute,
|
||||
scope *session.SessionScope,
|
||||
) *TurnContext {
|
||||
if inbound == nil && route == nil && scope == nil {
|
||||
return nil
|
||||
}
|
||||
return &TurnContext{
|
||||
Inbound: cloneInboundContext(inbound),
|
||||
Route: cloneResolvedRoute(route),
|
||||
Scope: session.CloneScope(scope),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +35,8 @@ func cloneTurnContext(ctx *TurnContext) *TurnContext {
|
||||
}
|
||||
cloned := *ctx
|
||||
cloned.Inbound = cloneInboundContext(ctx.Inbound)
|
||||
cloned.Route = cloneResolvedRoute(ctx.Route)
|
||||
cloned.Scope = session.CloneScope(ctx.Scope)
|
||||
return &cloned
|
||||
}
|
||||
|
||||
@@ -48,6 +62,31 @@ func cloneStringMap(src map[string]string) map[string]string {
|
||||
}
|
||||
|
||||
func cloneEventMeta(meta EventMeta) EventMeta {
|
||||
meta.Context = cloneTurnContext(meta.Context)
|
||||
meta.turnContext = cloneTurnContext(meta.turnContext)
|
||||
return meta
|
||||
}
|
||||
|
||||
func cloneResolvedRoute(route *routing.ResolvedRoute) *routing.ResolvedRoute {
|
||||
if route == nil {
|
||||
return nil
|
||||
}
|
||||
cloned := *route
|
||||
cloned.SessionPolicy = routing.SessionPolicy{
|
||||
DMScope: route.SessionPolicy.DMScope,
|
||||
IdentityLinks: cloneIdentityLinks(route.SessionPolicy.IdentityLinks),
|
||||
}
|
||||
return &cloned
|
||||
}
|
||||
|
||||
func cloneIdentityLinks(src map[string][]string) map[string][]string {
|
||||
if len(src) == 0 {
|
||||
return nil
|
||||
}
|
||||
cloned := make(map[string][]string, len(src))
|
||||
for canonical, ids := range src {
|
||||
dup := make([]string, len(ids))
|
||||
copy(dup, ids)
|
||||
cloned[canonical] = dup
|
||||
}
|
||||
return cloned
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user