Skip to content
This repository was archived by the owner on Sep 28, 2025. It is now read-only.
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@ result
tandem
.opencode
OpenCode.md
.predicted_expected_output.json
.predicted_expected_output.json
kali
7 changes: 2 additions & 5 deletions .tandem/swarm.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,15 @@
"debug": true,
"providers": {
"gemini": {
"apiKey": "${GEMINI_API_KEY}"
},
"groq": {
"apiKey": "${GROQ_API_KEY}"
"apiKey": "AIzaSyCokTJo3GZB520Ku3Xbd7aSBMF2S9RIX4c"
}
},
"agents": {
"orchestrator": {
"name": "orchestrator",
"agentId": "orchestrator",
"model": "gemini-2.5-flash",
"description": "an agents orchestrator in a multi-agent penetration testing firm run autonomously by AI.",
"description": "an agents orchestrator in a multi-agent penetration testing firm called as Tandem run autonomously by AI.",
"goal": "Assign penetration test related tasks like performing particular types of scans, searching for vulnerabilities and assessing them, searching for exploits and using them, gaining foothold post exploitation, documenting all the findings during the engagement etc to ai agents.",
"instructions": [
"tasks assignments should include what tools and techniques to be used by the subagent",
Expand Down
98 changes: 98 additions & 0 deletions SUBAGENT_ACTIVITY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
# SubAgent Activity Monitoring

This feature allows users to view real-time activity of subagents and abort tasks when needed.

## Overview

The SubAgent Activity Monitoring system provides:

- **Real-time Activity Monitoring**: View what each subagent is doing in real-time
- **Intelligent Status Updates**: LLM-generated status messages that are context-aware
- **Task Abortion**: Cancel running subagent tasks with a hotkey
- **Progress Tracking**: Visual progress indicators with estimated time remaining
- **Multi-agent Support**: Track multiple concurrent subagent activities

## Usage

### Accessing the Activity Monitor

Press `Ctrl+T` from the main chat interface to open the SubAgent Activity page.

### Activity Page Features

The activity page displays a table with the following columns:

- **Agent**: The type of subagent (Reconnoiter, VulnerabilityScanner, Exploiter, Reporter)
- **Task**: Brief description of the assigned task
- **Status**: Current status with visual indicators and intelligent descriptions
- **Progress**: Percentage completion
- **ETA**: Estimated time remaining
- **Started**: Time when the task began
- **Duration**: How long the task has been running

### Visual Status Indicators

- 🔄 **Starting**: Task is initializing
- ⚡ **Running**: Task is actively executing
- ✅ **Completed**: Task finished successfully
- ❌ **Error**: Task encountered an error
- 🛑 **Aborted**: Task was cancelled by user

### Keyboard Controls

- `Ctrl+T`: Open SubAgent Activity page
- `Ctrl+A`: Abort selected task
- `R`: Refresh activity list
- `Esc`: Return to chat page

## Intelligent Status Messages

The system generates context-aware status messages based on the agent type:

### Reconnoiter Agent
- "Identifying target systems and services..."
- "Enumerating open ports and services..."
- "Mapping network topology and discovering hosts..."

### Vulnerability Scanner
- "Loading vulnerability signatures and patterns..."
- "Scanning for common vulnerabilities (CVEs)..."
- "Testing for authentication bypasses and injection flaws..."

### Exploiter
- "Analyzing identified vulnerabilities for exploitation..."
- "Selecting appropriate exploit techniques and payloads..."
- "Attempting controlled exploitation within RoE boundaries..."

### Reporter
- "Gathering findings from all assessment phases..."
- "Analyzing and correlating vulnerability data..."
- "Creating executive summary and technical details..."

## Architecture

### Key Components

1. **SubAgent Service** (`internal/subagent/activity.go`): Manages activity tracking and events
2. **Activity Page** (`internal/tui/page/activity.go`): TUI interface for monitoring
3. **Status Generator** (`internal/subagent/status_generator.go`): Generates intelligent status messages
4. **Agent Tool Integration** (`internal/agent/agenttool.go`): Hooks into subagent execution

### Event Flow

1. When orchestrator delegates a task, AgentTool starts tracking the activity
2. Activity Service publishes real-time events via pubsub system
3. Activity Page subscribes to events and updates the display
4. Users can abort tasks, which cancels the underlying agent execution

## Benefits

- **Transparency**: Users can see exactly what subagents are doing
- **Control**: Ability to abort tasks that seem problematic
- **Efficiency**: No need to wait for stuck or lengthy tasks
- **Trust**: Builds confidence through visibility into AI agent behavior
- **Debugging**: Helps identify issues with agent performance

## Technical Details

The system uses Go's context cancellation for safe task abortion and a pubsub event system for real-time updates. All activity data is stored in memory and automatically cleaned up after completion.
10 changes: 0 additions & 10 deletions flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,6 @@
vagrant
starship
sqlc
gh

# TODO:
# docker_25
# vbox provider.
];

shellHook = ''
Expand All @@ -36,11 +31,6 @@
export PATH="$GOPATH/bin:$PATH"
export GOBIN="$GOPATH/bin"
eval "$(starship init bash)"

# TODO:
# include dockerd and docker start cmds.
# build the kali image and load it into docker.
# install the project level deps.
'';
};
};
Expand Down
70 changes: 65 additions & 5 deletions internal/agent/agenttool.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"github.com/yaydraco/tandem/internal/logging"
"github.com/yaydraco/tandem/internal/message"
"github.com/yaydraco/tandem/internal/session"
"github.com/yaydraco/tandem/internal/subagent"
"github.com/yaydraco/tandem/internal/tools"
)

Expand All @@ -29,8 +30,9 @@ type AgentToolArgs struct {
}

type AgentTool struct {
messages message.Service
sessions session.Service
messages message.Service
sessions session.Service
subagents subagent.Service
}

func (a *AgentTool) Info() tools.ToolInfo {
Expand Down Expand Up @@ -84,47 +86,105 @@ func (a *AgentTool) Run(ctx context.Context, call tools.ToolCall) (tools.ToolRes
return tools.ToolResponse{}, fmt.Errorf("error creating session: %s", err)
}

done, err := agent.Run(ctx, session.ID, args.Prompt)
// Start tracking the activity
activity, err := a.subagents.StartActivity(ctx, session.ID, sessionID, args.AgentName, args.Prompt)
if err != nil {
logging.Error("Failed to start activity tracking", err)
// Continue without activity tracking if it fails
}

// Create a cancellable context for this specific task
taskCtx, taskCancel := context.WithCancel(ctx)
defer taskCancel()

// Store the cancel function in the activity service
if activity != nil {
a.subagents.SetCancelFunc(activity.ID, taskCancel)
}

// Update activity status to running
if activity != nil {
a.subagents.UpdateActivity(ctx, activity.ID, subagent.StatusRunning, "", "10%")
}

done, err := agent.Run(taskCtx, session.ID, args.Prompt)
if err != nil {
if activity != nil {
a.subagents.CompleteActivity(ctx, activity.ID, false, fmt.Sprintf("error generating agent: %s", err))
}
return tools.ToolResponse{}, fmt.Errorf("error generating agent: %s", err)
}

logging.Debug("using agent", "name", args.AgentName, "busy", agent.IsBusy())

// Update progress
if activity != nil {
a.subagents.UpdateActivity(ctx, activity.ID, subagent.StatusRunning, "", "80%")
}

result := <-done
logging.Debug("task done by agent", "name", args.AgentName, "busy", agent.IsBusy())

if result.Error != nil {
if activity != nil {
if result.Error.Error() == "request cancelled by user" || result.Error.Error() == "context canceled" {
// Activity was already marked as aborted by the cancel function
return tools.ToolResponse{}, fmt.Errorf("task was aborted by user")
}
a.subagents.CompleteActivity(ctx, activity.ID, false, fmt.Sprintf("error generating agent: %s", result.Error))
}
return tools.ToolResponse{}, fmt.Errorf("error generating agent: %s", result.Error)
}

response := result.Message
if response.Role != message.Assistant {
if activity != nil {
a.subagents.CompleteActivity(ctx, activity.ID, false, "no response")
}
return tools.NewTextErrorResponse("no response"), nil
}

updatedSession, err := a.sessions.Get(ctx, session.ID)
if err != nil {
if activity != nil {
a.subagents.CompleteActivity(ctx, activity.ID, false, fmt.Sprintf("error getting session: %s", err))
}
return tools.ToolResponse{}, fmt.Errorf("error getting session: %s", err)
}
parentSession, err := a.sessions.Get(ctx, sessionID)
if err != nil {
if activity != nil {
a.subagents.CompleteActivity(ctx, activity.ID, false, fmt.Sprintf("error getting parent session: %s", err))
}
return tools.ToolResponse{}, fmt.Errorf("error getting parent session: %s", err)
}

parentSession.Cost += updatedSession.Cost

_, err = a.sessions.Save(ctx, parentSession)
if err != nil {
if activity != nil {
a.subagents.CompleteActivity(ctx, activity.ID, false, fmt.Sprintf("error saving parent session: %s", err))
}
return tools.ToolResponse{}, fmt.Errorf("error saving parent session: %s", err)
}

// Mark activity as completed successfully
if activity != nil {
a.subagents.CompleteActivity(ctx, activity.ID, true, "Task completed successfully")
}

return tools.NewTextResponse(response.Content().String()), nil
}

func NewAgentTool(
Sessions session.Service,
Messages message.Service,
SubAgents subagent.Service,
) tools.BaseTool {
return &AgentTool{
sessions: Sessions,
messages: Messages,
sessions: Sessions,
messages: Messages,
subagents: SubAgents,
}
}
10 changes: 7 additions & 3 deletions internal/app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,32 +13,36 @@ import (
"github.com/yaydraco/tandem/internal/logging"
"github.com/yaydraco/tandem/internal/message"
"github.com/yaydraco/tandem/internal/session"
"github.com/yaydraco/tandem/internal/subagent"
"github.com/yaydraco/tandem/internal/tools"
)

type App struct {
Sessions session.Service
Messages message.Service
Orchestrator agent.Service
SubAgents subagent.Service
// ADHD: why we shouldn't initialise all the agents at once right in here? here's another thought. we don't want to have multiple agents of the same time, say couple of reconnoiters, doing some scanning because of the nature of the task in hand.
}

func New(ctx context.Context, conn *sql.DB) (*App, error) {
q := db.New(conn)
sessions := session.NewService(q)
messages := message.NewService(q)
subagents := subagent.NewService()

app := &App{
Sessions: sessions,
Messages: messages,
Sessions: sessions,
Messages: messages,
SubAgents: subagents,
}

var err error
app.Orchestrator, err = agent.NewAgent(
config.Orchestrator,
app.Sessions,
app.Messages,
[]tools.BaseTool{agent.NewAgentTool(app.Sessions, app.Messages)},
[]tools.BaseTool{agent.NewAgentTool(app.Sessions, app.Messages, app.SubAgents)},
nil,
)

Expand Down
1 change: 1 addition & 0 deletions internal/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,7 @@ func setupSubscriptions(app *app.App, parentCtx context.Context) (chan tea.Msg,
setupSubscriber(ctx, &wg, "sessions", app.Sessions.Subscribe, ch)
setupSubscriber(ctx, &wg, "messages", app.Messages.Subscribe, ch)
setupSubscriber(ctx, &wg, "orchestrator", app.Orchestrator.Subscribe, ch)
setupSubscriber(ctx, &wg, "subagents", app.SubAgents.Subscribe, ch)

cleanupFunc := func() {
logging.Info("Cancelling all subscriptions")
Expand Down
Loading