mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-08-19 22:13:31 +00:00
Bound Patrol evaluator finding writes
This commit is contained in:
parent
67bbed4c40
commit
7f80f29db7
14 changed files with 255 additions and 55 deletions
|
|
@ -6296,8 +6296,9 @@ sessions, fleet policy, or update state.
|
|||
### Patrol follow-up manifests do not confer agent authority
|
||||
|
||||
`internal/api/chat_service_adapter.go` now forwards a structured tool-name
|
||||
allowlist for bounded Patrol continuations. This list is AI-runtime metadata
|
||||
that can only reduce the already projected detection manifest. It does not
|
||||
allowlist and successful finding-report cap for bounded Patrol continuations.
|
||||
These are AI-runtime metadata that can only reduce the already projected
|
||||
detection manifest and model-owned Pulse-state writes. They do not
|
||||
enroll an agent, select an agent identity, enable commands, grant a command
|
||||
session, alter fleet policy, or bypass agent-side preflight and action
|
||||
admission. Agent lifecycle and command authority remain unchanged.
|
||||
|
|
|
|||
|
|
@ -7166,6 +7166,13 @@ complete snapshot exists it receives only `patrol_get_findings` plus
|
|||
`patrol_assess_finding`. These are structured call-site allowlists applied
|
||||
after profile projection and may only reduce authority. An unavailable or
|
||||
unknown requested tool fails closed. Prompt text never selects the manifest.
|
||||
The evaluation pass also carries a typed successful-report budget equal to the
|
||||
number of unmatched signals it was given. Accepted `patrol_report_finding`
|
||||
writes consume that budget; once it is exhausted, report authority is removed
|
||||
and the next provider turn is a tool-free bounded summary. Capped same-turn
|
||||
report batches execute in provider order, and any excess call fails before
|
||||
persistence. Ordinary Watch runs carry no report cap, so model-owned discovery
|
||||
of an open-ended number of independent problems remains intact.
|
||||
Each Patrol invocation, including the main pass and both continuations, receives
|
||||
a fresh infrastructure workflow FSM and resolved-resource context. Its stable
|
||||
session ID remains a forensic-log key only and cannot carry a prior run's read
|
||||
|
|
|
|||
|
|
@ -9469,10 +9469,13 @@ the boundary and its published shape.
|
|||
### Internal Patrol bridge preserves bounded follow-up authority
|
||||
|
||||
`internal/api/chat_service_adapter.go` forwards the AI runtime's structured
|
||||
`allowed_tool_names` field across the internal Patrol-to-chat boundary without
|
||||
deriving it from prompts or widening it. The chat service applies the list only
|
||||
after the Patrol detection profile has projected its canonical tool manifest;
|
||||
the list can remove tools but cannot add one, and unknown names fail closed.
|
||||
`allowed_tool_names` and `max_finding_reports` fields across the internal
|
||||
Patrol-to-chat boundary without deriving either from prompts or widening them.
|
||||
The chat service applies the list only after the Patrol detection profile has
|
||||
projected its canonical tool manifest; the list can remove tools but cannot add
|
||||
one, and unknown names fail closed. A positive report cap bounds successful
|
||||
`patrol_report_finding` persistence for the invocation and removes report
|
||||
authority when reached; zero preserves the ordinary uncapped Watch contract.
|
||||
This is an internal execution contract, not a new HTTP field or client-granted
|
||||
capability. Existing Patrol routes, authorization, tenant binding, payloads,
|
||||
and action approval semantics are unchanged.
|
||||
|
|
|
|||
|
|
@ -5239,8 +5239,9 @@ and authorized recovery behavior.
|
|||
|
||||
### Patrol follow-up manifests do not confer storage authority
|
||||
|
||||
The structured bounded-tool allowlist forwarded by
|
||||
`internal/api/chat_service_adapter.go` is an AI-runtime authority reduction.
|
||||
It adds no storage, backup, snapshot, restore, retention, or recovery tool and
|
||||
cannot turn a detection pass into a mutation path. Storage and recovery state,
|
||||
evidence freshness, persistence, and admission contracts remain unchanged.
|
||||
The structured bounded-tool allowlist and successful finding-report cap
|
||||
forwarded by `internal/api/chat_service_adapter.go` are AI-runtime authority
|
||||
reductions. They add no storage, backup, snapshot, restore, retention, or
|
||||
recovery tool and cannot turn a detection pass into a mutation path. Storage
|
||||
and recovery state, evidence freshness, persistence, and admission contracts
|
||||
remain unchanged.
|
||||
|
|
|
|||
|
|
@ -386,6 +386,15 @@ func isPatrolFindingLifecycleWrite(toolName string) bool {
|
|||
}
|
||||
}
|
||||
|
||||
func containsPatrolFindingReport(toolCalls []providers.ToolCall) bool {
|
||||
for _, tc := range toolCalls {
|
||||
if strings.TrimSpace(tc.Name) == agentcapabilities.PatrolReportFindingToolName {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// requiresOrderedPatrolFindingLifecycleExecution identifies a same-turn
|
||||
// read-before-write dependency. Independent reads and independent finding
|
||||
// writes may still run in parallel, but a lifecycle write cannot race the
|
||||
|
|
@ -670,6 +679,7 @@ type AgenticLoop struct {
|
|||
baseSystemPrompt string // Base prompt without mode context
|
||||
maxTurns int
|
||||
maxEvidenceCalls int
|
||||
maxFindingReports int
|
||||
orgID string
|
||||
executionID string
|
||||
streamIdleTimeout time.Duration
|
||||
|
|
@ -795,6 +805,7 @@ func (a *AgenticLoop) executeWithTools(ctx context.Context, sessionID string, me
|
|||
// before calling ExecuteWithTools, and this avoids races with concurrent sessions.
|
||||
a.mu.Lock()
|
||||
maxTurns := a.maxTurns
|
||||
maxFindingReports := a.maxFindingReports
|
||||
suppressProviderErrorEvents := a.suppressProviderErrorEvents
|
||||
a.aborted[sessionID] = false
|
||||
a.mu.Unlock()
|
||||
|
|
@ -827,6 +838,7 @@ func (a *AgenticLoop) executeWithTools(ctx context.Context, sessionID string, me
|
|||
patrolFindingRepairAttempted := false // The repair-only extension is bounded to one provider turn
|
||||
toolBlockedLastTurn := false // When true, request final text after budget/loop block
|
||||
investigationProposalCompleted := false
|
||||
acceptedFindingReports := 0
|
||||
|
||||
// Loop detection: track identical tool calls (name + serialized input).
|
||||
// After maxIdenticalCalls identical invocations, the next one is blocked.
|
||||
|
|
@ -1953,7 +1965,11 @@ func (a *AgenticLoop) executeWithTools(ctx context.Context, sessionID string, me
|
|||
emitWorkflowState(callback, "execute", executeMessage, sessionFSMState(fsm), workflowTool)
|
||||
}
|
||||
|
||||
orderedPatrolLifecycle := requiresOrderedPatrolFindingLifecycleExecution(toolCalls)
|
||||
// A capped evaluator must apply its accepted-report budget between
|
||||
// sibling calls. Keep those batches ordered so a same-turn excess can
|
||||
// never race past the persistence boundary.
|
||||
orderedPatrolLifecycle := requiresOrderedPatrolFindingLifecycleExecution(toolCalls) ||
|
||||
(maxFindingReports > 0 && containsPatrolFindingReport(toolCalls))
|
||||
if len(pendingExec) > 1 && !orderedPatrolLifecycle {
|
||||
log.Info().
|
||||
Int("tool_count", len(pendingExec)).
|
||||
|
|
@ -1982,8 +1998,22 @@ func (a *AgenticLoop) executeWithTools(ctx context.Context, sessionID string, me
|
|||
Msg("[AgenticLoop] Executing ordered Patrol finding lifecycle batch")
|
||||
}
|
||||
for j, pe := range pendingExec {
|
||||
findingReport := strings.TrimSpace(pe.tc.Name) == agentcapabilities.PatrolReportFindingToolName
|
||||
if findingReport &&
|
||||
maxFindingReports > 0 && acceptedFindingReports >= maxFindingReports {
|
||||
execResults[j] = parallelToolResult{Result: agentcapabilities.NewToolJSONResultWithIsError(map[string]interface{}{
|
||||
"error": map[string]interface{}{
|
||||
"code": "PATROL_FINDING_REPORT_BUDGET_EXHAUSTED",
|
||||
"message": fmt.Sprintf("This evaluation has already accepted its maximum of %d finding reports. Summarize the accepted results without another report call.", maxFindingReports),
|
||||
},
|
||||
}, true)}
|
||||
continue
|
||||
}
|
||||
r, e := a.executeToolSafely(ctx, pe.tc.ID, pe.tc.Name, pe.tc.Input)
|
||||
execResults[j] = parallelToolResult{Result: r, Err: e}
|
||||
if findingReport && e == nil && !r.IsError {
|
||||
acceptedFindingReports++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -2293,9 +2323,10 @@ func (a *AgenticLoop) executeWithTools(ctx context.Context, sessionID string, me
|
|||
if patrolFindingLifecycleSucceededThisTurn {
|
||||
if a.currentExecutionProfile().NonInteractive() {
|
||||
patrolFindingSummaryPending = true
|
||||
if patrolFindingLifecycleFailedThisTurn && !patrolFindingRepairTurn && !patrolFindingRepairAttempted {
|
||||
reportBudgetExhausted := maxFindingReports > 0 && acceptedFindingReports >= maxFindingReports
|
||||
if patrolFindingLifecycleFailedThisTurn && !reportBudgetExhausted && !patrolFindingRepairTurn && !patrolFindingRepairAttempted {
|
||||
patrolFindingRepairPending = true
|
||||
} else if !patrolFindingRepairTurn {
|
||||
} else if !reportBudgetExhausted && !patrolFindingRepairTurn {
|
||||
patrolFindingContinuationPending = true
|
||||
}
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -45,6 +45,10 @@ func TestAgenticLoop_Setters(t *testing.T) {
|
|||
if loop.maxEvidenceCalls != 5 {
|
||||
t.Fatalf("expected maxEvidenceCalls=5, got %d", loop.maxEvidenceCalls)
|
||||
}
|
||||
loop.SetMaxFindingReports(3)
|
||||
if loop.maxFindingReports != 3 {
|
||||
t.Fatalf("expected maxFindingReports=3, got %d", loop.maxFindingReports)
|
||||
}
|
||||
|
||||
loop.SetProviderInfo("provider", "model")
|
||||
if loop.providerName != "provider" || loop.modelName != "model" {
|
||||
|
|
@ -928,6 +932,132 @@ func TestAgenticLoopAllowsSequentialIndependentWatchFindings(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestAgenticLoopStopsFindingContinuationAtAcceptedReportBudget(t *testing.T) {
|
||||
provider := &stubStreamingProvider{}
|
||||
executor := tools.NewPulseToolExecutor(tools.ExecutorConfig{})
|
||||
executor.ApplyExecutionProfile(tools.ProfilePatrolDetection)
|
||||
creator := &repairTestPatrolFindingCreator{checked: true}
|
||||
executor.SetPatrolFindingCreator(creator)
|
||||
loop := NewAgenticLoop(provider, executor, "focused Patrol evaluator prompt")
|
||||
loop.SetExecutionProfile(tools.ProfilePatrolDetection)
|
||||
loop.SetMaxTurns(5)
|
||||
loop.SetMaxFindingReports(2)
|
||||
|
||||
report := func(resourceID, resourceName, title string) map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"key": "container-health-failed",
|
||||
"severity": "warning",
|
||||
"category": "reliability",
|
||||
"resource_id": resourceID,
|
||||
"resource_name": resourceName,
|
||||
"resource_type": "app-container",
|
||||
"title": title,
|
||||
"description": "Container is running but its health check is unhealthy.",
|
||||
"recommendation": "Inspect the health endpoint and recent logs.",
|
||||
"evidence": "Provider state reports running and unhealthy with zero restarts.",
|
||||
}
|
||||
}
|
||||
|
||||
providerCalls := 0
|
||||
provider.chatStream = func(_ context.Context, req providers.ChatRequest, callback providers.StreamCallback) error {
|
||||
providerCalls++
|
||||
switch providerCalls {
|
||||
case 1:
|
||||
call := providers.ToolCall{ID: "report-api", Name: agentcapabilities.PatrolReportFindingToolName, Input: report("app-container-api", "api", "API health check failing")}
|
||||
callback(providers.StreamEvent{Type: "done", Data: providers.DoneEvent{ToolCalls: []providers.ToolCall{call}}})
|
||||
case 2:
|
||||
if req.System != patrolFindingLifecycleContinuationSystemPrompt || len(req.Tools) != 1 {
|
||||
t.Fatalf("second evaluator request = %+v, want report-only continuation", req)
|
||||
}
|
||||
call := providers.ToolCall{ID: "report-worker", Name: agentcapabilities.PatrolReportFindingToolName, Input: report("app-container-worker", "worker", "Worker health check failing")}
|
||||
callback(providers.StreamEvent{Type: "done", Data: providers.DoneEvent{ToolCalls: []providers.ToolCall{call}}})
|
||||
case 3:
|
||||
if req.System != patrolFindingLifecycleSummarySystemPrompt || len(req.Tools) != 0 || req.ToolChoice != nil {
|
||||
t.Fatalf("post-budget evaluator request = %+v, want tool-free bounded summary", req)
|
||||
}
|
||||
callback(providers.StreamEvent{Type: "content", Data: providers.ContentEvent{Text: "Two findings recorded."}})
|
||||
callback(providers.StreamEvent{Type: "done", Data: providers.DoneEvent{}})
|
||||
default:
|
||||
t.Fatalf("unexpected provider call %d; report authority survived its accepted-write budget", providerCalls)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
available := []providers.Tool{{Name: agentcapabilities.PatrolReportFindingToolName}}
|
||||
result, err := loop.ExecuteWithTools(context.Background(), "bounded-finding-evaluator", []Message{{Role: "user", Content: "Evaluate two unmatched signals."}}, available, func(StreamEvent) {})
|
||||
if err != nil {
|
||||
t.Fatalf("bounded finding evaluator failed: %v", err)
|
||||
}
|
||||
if providerCalls != 3 {
|
||||
t.Fatalf("provider calls = %d, want two reports and one summary", providerCalls)
|
||||
}
|
||||
if len(creator.created) != 2 || creator.created[0].ResourceID != "app-container-api" || creator.created[1].ResourceID != "app-container-worker" {
|
||||
t.Fatalf("created findings = %+v, want exactly the two budgeted reports", creator.created)
|
||||
}
|
||||
if len(result) == 0 || result[len(result)-1].Content != "Two findings recorded." {
|
||||
t.Fatalf("final result = %+v", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAgenticLoopRejectsSameTurnFindingReportsBeyondBudget(t *testing.T) {
|
||||
provider := &stubStreamingProvider{}
|
||||
executor := tools.NewPulseToolExecutor(tools.ExecutorConfig{})
|
||||
executor.ApplyExecutionProfile(tools.ProfilePatrolDetection)
|
||||
creator := &repairTestPatrolFindingCreator{checked: true}
|
||||
executor.SetPatrolFindingCreator(creator)
|
||||
loop := NewAgenticLoop(provider, executor, "focused Patrol evaluator prompt")
|
||||
loop.SetExecutionProfile(tools.ProfilePatrolDetection)
|
||||
loop.SetMaxTurns(2)
|
||||
loop.SetMaxFindingReports(1)
|
||||
|
||||
report := func(id string) map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"key": "container-health-failed", "severity": "warning", "category": "reliability",
|
||||
"resource_id": id, "resource_name": id, "resource_type": "app-container",
|
||||
"title": "Health check failing", "description": "Current health check is failing.",
|
||||
"recommendation": "Inspect the health endpoint.", "evidence": "Provider reports unhealthy.",
|
||||
}
|
||||
}
|
||||
providerCalls := 0
|
||||
provider.chatStream = func(_ context.Context, req providers.ChatRequest, callback providers.StreamCallback) error {
|
||||
providerCalls++
|
||||
if providerCalls == 1 {
|
||||
callback(providers.StreamEvent{Type: "done", Data: providers.DoneEvent{ToolCalls: []providers.ToolCall{
|
||||
{ID: "report-accepted", Name: agentcapabilities.PatrolReportFindingToolName, Input: report("app-container-api")},
|
||||
{ID: "report-excess", Name: agentcapabilities.PatrolReportFindingToolName, Input: report("app-container-worker")},
|
||||
}}})
|
||||
return nil
|
||||
}
|
||||
if len(req.Tools) != 0 || req.System != patrolFindingLifecycleSummarySystemPrompt {
|
||||
t.Fatalf("post-budget request = %+v, want tool-free summary", req)
|
||||
}
|
||||
callback(providers.StreamEvent{Type: "content", Data: providers.ContentEvent{Text: "One bounded finding recorded."}})
|
||||
callback(providers.StreamEvent{Type: "done", Data: providers.DoneEvent{}})
|
||||
return nil
|
||||
}
|
||||
|
||||
var toolEnds []ToolEndData
|
||||
_, err := loop.ExecuteWithTools(context.Background(), "same-turn-report-budget", []Message{{Role: "user", Content: "Evaluate one unmatched signal."}}, []providers.Tool{{Name: agentcapabilities.PatrolReportFindingToolName}}, func(event StreamEvent) {
|
||||
if event.Type != "tool_end" {
|
||||
return
|
||||
}
|
||||
var data ToolEndData
|
||||
if err := json.Unmarshal(event.Data, &data); err != nil {
|
||||
t.Fatalf("decode tool_end: %v", err)
|
||||
}
|
||||
toolEnds = append(toolEnds, data)
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("same-turn report budget run failed: %v", err)
|
||||
}
|
||||
if len(creator.created) != 1 || creator.created[0].ResourceID != "app-container-api" {
|
||||
t.Fatalf("persisted reports = %+v, want only first budgeted call", creator.created)
|
||||
}
|
||||
if len(toolEnds) != 2 || !toolEnds[0].Success || toolEnds[1].Success || !strings.Contains(toolEnds[1].Output, "PATROL_FINDING_REPORT_BUDGET_EXHAUSTED") {
|
||||
t.Fatalf("tool results = %+v, want accepted first report and fail-closed excess", toolEnds)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnsureFinalTextResponse(t *testing.T) {
|
||||
provider := &stubStreamingProvider{}
|
||||
loop := &AgenticLoop{provider: provider, baseSystemPrompt: "prompt"}
|
||||
|
|
|
|||
|
|
@ -107,6 +107,15 @@ func (a *AgenticLoop) SetMaxEvidenceCalls(n int) {
|
|||
a.mu.Unlock()
|
||||
}
|
||||
|
||||
// SetMaxFindingReports bounds successful patrol_report_finding writes for a
|
||||
// focused Patrol invocation. A non-positive value leaves ordinary Watch runs
|
||||
// uncapped; evaluator passes set the exact number of unmatched signals.
|
||||
func (a *AgenticLoop) SetMaxFindingReports(n int) {
|
||||
a.mu.Lock()
|
||||
a.maxFindingReports = n
|
||||
a.mu.Unlock()
|
||||
}
|
||||
|
||||
// SetProviderInfo sets the provider/model info for telemetry.
|
||||
func (a *AgenticLoop) SetProviderInfo(provider, model string) {
|
||||
a.mu.Lock()
|
||||
|
|
|
|||
|
|
@ -2783,13 +2783,14 @@ func (s *Service) hydrateHandoffResources(sessionID string, handoffResources []H
|
|||
|
||||
// PatrolRequest represents a patrol execution request within the chat service
|
||||
type PatrolRequest struct {
|
||||
Prompt string `json:"prompt"`
|
||||
SystemPrompt string `json:"system_prompt"`
|
||||
SessionID string `json:"session_id,omitempty"`
|
||||
ExecutionID string `json:"execution_id,omitempty"`
|
||||
UseCase string `json:"use_case"`
|
||||
MaxTurns int `json:"max_turns,omitempty"`
|
||||
AllowedToolNames []string `json:"allowed_tool_names,omitempty"`
|
||||
Prompt string `json:"prompt"`
|
||||
SystemPrompt string `json:"system_prompt"`
|
||||
SessionID string `json:"session_id,omitempty"`
|
||||
ExecutionID string `json:"execution_id,omitempty"`
|
||||
UseCase string `json:"use_case"`
|
||||
MaxTurns int `json:"max_turns,omitempty"`
|
||||
MaxFindingReports int `json:"max_finding_reports,omitempty"`
|
||||
AllowedToolNames []string `json:"allowed_tool_names,omitempty"`
|
||||
}
|
||||
|
||||
// PatrolResponse contains the results of a patrol execution
|
||||
|
|
@ -2865,6 +2866,9 @@ func (s *Service) ExecutePatrolStream(ctx context.Context, req PatrolRequest, ca
|
|||
if req.MaxTurns > 0 {
|
||||
tempLoop.SetMaxTurns(req.MaxTurns)
|
||||
}
|
||||
if req.MaxFindingReports > 0 {
|
||||
tempLoop.SetMaxFindingReports(req.MaxFindingReports)
|
||||
}
|
||||
|
||||
// Set provider info for telemetry
|
||||
parts := strings.SplitN(patrolModel, ":", 2)
|
||||
|
|
|
|||
|
|
@ -286,7 +286,7 @@ func TestService_ExecutePatrolStream_UsesFreshFSMAndAcceptsCoreValidatedFindingW
|
|||
}
|
||||
providerCalls := 0
|
||||
service.providerFactory = func(string) (providers.StreamingProvider, error) {
|
||||
return &mockStreamingProvider{chatStreamFunc: func(_ context.Context, _ providers.ChatRequest, callback providers.StreamCallback) error {
|
||||
return &mockStreamingProvider{chatStreamFunc: func(_ context.Context, req providers.ChatRequest, callback providers.StreamCallback) error {
|
||||
providerCalls++
|
||||
if providerCalls == 1 {
|
||||
callback(providers.StreamEvent{Type: "done", Data: providers.DoneEvent{
|
||||
|
|
@ -310,6 +310,9 @@ func TestService_ExecutePatrolStream_UsesFreshFSMAndAcceptsCoreValidatedFindingW
|
|||
}})
|
||||
return nil
|
||||
}
|
||||
if req.System != patrolFindingLifecycleSummarySystemPrompt || len(req.Tools) != 0 || req.ToolChoice != nil {
|
||||
t.Fatalf("post-budget service request = %+v, want tool-free Patrol summary", req)
|
||||
}
|
||||
callback(providers.StreamEvent{Type: "content", Data: providers.ContentEvent{Text: "Finding recorded."}})
|
||||
callback(providers.StreamEvent{Type: "done", Data: providers.DoneEvent{InputTokens: 4, OutputTokens: 2}})
|
||||
return nil
|
||||
|
|
@ -317,10 +320,11 @@ func TestService_ExecutePatrolStream_UsesFreshFSMAndAcceptsCoreValidatedFindingW
|
|||
}
|
||||
|
||||
resp, err := service.ExecutePatrolStream(context.Background(), PatrolRequest{
|
||||
Prompt: "Record the validated signal.",
|
||||
SessionID: "patrol-eval",
|
||||
MaxTurns: 3,
|
||||
AllowedToolNames: []string{agentcapabilities.PatrolReportFindingToolName},
|
||||
Prompt: "Record the validated signal.",
|
||||
SessionID: "patrol-eval",
|
||||
MaxTurns: 3,
|
||||
MaxFindingReports: 1,
|
||||
AllowedToolNames: []string{agentcapabilities.PatrolReportFindingToolName},
|
||||
}, func(StreamEvent) {})
|
||||
if err != nil {
|
||||
t.Fatalf("ExecutePatrolStream failed: %v", err)
|
||||
|
|
|
|||
|
|
@ -1363,13 +1363,14 @@ func (p *PatrolService) runEvaluationPass(ctx context.Context, adapter *patrolFi
|
|||
|
||||
trace := newPatrolFollowupTraceCollector("evaluation")
|
||||
resp, err := cs.ExecutePatrolStream(ctx, PatrolExecuteRequest{
|
||||
Prompt: userPrompt,
|
||||
SystemPrompt: systemPrompt,
|
||||
SessionID: "patrol-eval",
|
||||
ExecutionID: executionID,
|
||||
UseCase: "patrol",
|
||||
MaxTurns: 5,
|
||||
AllowedToolNames: allowedToolNames,
|
||||
Prompt: userPrompt,
|
||||
SystemPrompt: systemPrompt,
|
||||
SessionID: "patrol-eval",
|
||||
ExecutionID: executionID,
|
||||
UseCase: "patrol",
|
||||
MaxTurns: 5,
|
||||
MaxFindingReports: len(unmatchedSignals),
|
||||
AllowedToolNames: allowedToolNames,
|
||||
}, trace.callback)
|
||||
if resp != nil {
|
||||
resp.ToolCalls = trace.records()
|
||||
|
|
|
|||
|
|
@ -129,6 +129,9 @@ func TestRunEvaluationPass(t *testing.T) {
|
|||
if !reflect.DeepEqual(captured.AllowedToolNames, wantTools) {
|
||||
t.Fatalf("evaluation tools = %v, want %v", captured.AllowedToolNames, wantTools)
|
||||
}
|
||||
if captured.MaxFindingReports != 1 {
|
||||
t.Fatalf("evaluation finding report budget = %d, want one unmatched signal", captured.MaxFindingReports)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunEvaluationPassReusesEstablishedFindingSnapshot(t *testing.T) {
|
||||
|
|
@ -153,6 +156,9 @@ func TestRunEvaluationPassReusesEstablishedFindingSnapshot(t *testing.T) {
|
|||
if !reflect.DeepEqual(captured.AllowedToolNames, []string{agentcapabilities.PatrolReportFindingToolName}) {
|
||||
t.Fatalf("evaluation tools = %v, want report only", captured.AllowedToolNames)
|
||||
}
|
||||
if captured.MaxFindingReports != 1 {
|
||||
t.Fatalf("evaluation finding report budget = %d, want one unmatched signal", captured.MaxFindingReports)
|
||||
}
|
||||
if !strings.Contains(captured.Prompt, "finding-1") || !strings.Contains(captured.SystemPrompt, "already included") {
|
||||
t.Fatalf("evaluation did not reuse established snapshot: system=%q prompt=%q", captured.SystemPrompt, captured.Prompt)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -200,13 +200,14 @@ type ChatToolResult = agentcapabilities.ProviderToolResult
|
|||
|
||||
// PatrolExecuteRequest represents a patrol execution request via the chat service
|
||||
type PatrolExecuteRequest struct {
|
||||
Prompt string `json:"prompt"`
|
||||
SystemPrompt string `json:"system_prompt"`
|
||||
SessionID string `json:"session_id,omitempty"`
|
||||
ExecutionID string `json:"execution_id,omitempty"`
|
||||
UseCase string `json:"use_case"` // "patrol" — for model selection
|
||||
MaxTurns int `json:"max_turns,omitempty"`
|
||||
AllowedToolNames []string `json:"allowed_tool_names,omitempty"`
|
||||
Prompt string `json:"prompt"`
|
||||
SystemPrompt string `json:"system_prompt"`
|
||||
SessionID string `json:"session_id,omitempty"`
|
||||
ExecutionID string `json:"execution_id,omitempty"`
|
||||
UseCase string `json:"use_case"` // "patrol" — for model selection
|
||||
MaxTurns int `json:"max_turns,omitempty"`
|
||||
MaxFindingReports int `json:"max_finding_reports,omitempty"`
|
||||
AllowedToolNames []string `json:"allowed_tool_names,omitempty"`
|
||||
}
|
||||
|
||||
// QuickAnalysisRequest represents a lightweight single-turn analysis request.
|
||||
|
|
|
|||
|
|
@ -54,13 +54,14 @@ func (a *chatServiceAdapter) ExecutePatrolStream(ctx context.Context, req ai.Pat
|
|||
|
||||
func adaptPatrolExecuteRequest(req ai.PatrolExecuteRequest) chat.PatrolRequest {
|
||||
return chat.PatrolRequest{
|
||||
Prompt: req.Prompt,
|
||||
SystemPrompt: req.SystemPrompt,
|
||||
SessionID: req.SessionID,
|
||||
ExecutionID: req.ExecutionID,
|
||||
UseCase: req.UseCase,
|
||||
MaxTurns: req.MaxTurns,
|
||||
AllowedToolNames: append([]string(nil), req.AllowedToolNames...),
|
||||
Prompt: req.Prompt,
|
||||
SystemPrompt: req.SystemPrompt,
|
||||
SessionID: req.SessionID,
|
||||
ExecutionID: req.ExecutionID,
|
||||
UseCase: req.UseCase,
|
||||
MaxTurns: req.MaxTurns,
|
||||
MaxFindingReports: req.MaxFindingReports,
|
||||
AllowedToolNames: append([]string(nil), req.AllowedToolNames...),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -73,15 +73,16 @@ func TestContractPatrolInternalBridgePreservesBoundedToolAuthority(t *testing.T)
|
|||
agentcapabilities.PatrolReportFindingToolName,
|
||||
}
|
||||
got := adaptPatrolExecuteRequest(ai.PatrolExecuteRequest{
|
||||
Prompt: "evaluate",
|
||||
SystemPrompt: "bounded",
|
||||
SessionID: "patrol-eval",
|
||||
ExecutionID: "run-1",
|
||||
UseCase: "patrol",
|
||||
MaxTurns: 5,
|
||||
AllowedToolNames: allowed,
|
||||
Prompt: "evaluate",
|
||||
SystemPrompt: "bounded",
|
||||
SessionID: "patrol-eval",
|
||||
ExecutionID: "run-1",
|
||||
UseCase: "patrol",
|
||||
MaxTurns: 5,
|
||||
MaxFindingReports: 2,
|
||||
AllowedToolNames: allowed,
|
||||
})
|
||||
if got.Prompt != "evaluate" || got.SystemPrompt != "bounded" || got.SessionID != "patrol-eval" || got.ExecutionID != "run-1" || got.UseCase != "patrol" || got.MaxTurns != 5 {
|
||||
if got.Prompt != "evaluate" || got.SystemPrompt != "bounded" || got.SessionID != "patrol-eval" || got.ExecutionID != "run-1" || got.UseCase != "patrol" || got.MaxTurns != 5 || got.MaxFindingReports != 2 {
|
||||
t.Fatalf("Patrol bridge lost execution metadata: %+v", got)
|
||||
}
|
||||
if !reflect.DeepEqual(got.AllowedToolNames, allowed) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue