# AgentWorks: Complete Product and Documentation Context Canonical website: https://agentworkshq.com/ Open-source repository: https://github.com/manishiitg/mcp-agent-builder-go Latest release: https://github.com/manishiitg/mcp-agent-builder-go/releases/latest AgentWorks is an open-source operating layer for coordinating, observing, and improving AI workflows across a company. It manages goals, schedules, model and CLI routing, browser and MCP tools, secrets, evidence, costs, Pulse, human approvals, reports, reusable skills, and bounded Auto Improve proposals. ## Documentation Canonical HTML: https://agentworkshq.com/docs/overview/ Raw Markdown: https://agentworkshq.com/docs-content/README.md # Documentation Start with the operator journey, then use the subsystem references when you need implementation detail. ## Start Here - [Getting Started](getting-started/README.md): install AgentWorks, complete first-launch setup, and create a first automation. - [Build Your First Workflow](getting-started/first-workflow.md): define an outcome, choose a worker, run it, review evidence, and improve the next run. ## Product Areas - [Workflow](workflow/README.md): workflow authoring, execution, scheduling, monitoring, Pulse, and Auto Improve. - [Organization and Agents](multiagent/README.md): delegation, Org Pulse, shared memory, and agent-to-agent coordination. - [Core](core/README.md): providers, MCP, browser sessions, connectors, secrets, security, and shared runtime services. `docs/bugs/` is an incident archive. `docs/refactor/` records implementation migrations. Neither folder is the recommended entry point for operators. ## Placement Rules - Put a doc in `workflow/` when it is primarily about workflow authoring, workflow execution, step configuration, or workflow-only UX. - Put a doc in `multiagent/` when it is primarily about manager/worker delegation, multi-agent chat, or agent-to-agent coordination. - Put a doc in `core/` when it applies across chat, workflow, and multi-agent modes or describes a foundational subsystem or integration. ## Current Sections ### Workflow - React Flow canvas and workflow UX - Running workflows and workflow manifests - Step config, execution modes, tool filtering, and runtime overrides - Validation, learning, evaluation, human feedback, and monitoring - Specialized workflow step types such as `todo_task` ### Organization and Agents - Sub-agent delegation - Multi-agent chat architecture - Agent memory - Slash-command entry points for delegation ### Core - Eventing, streaming, session propagation, auth, secrets, and workspace isolation - LLM provider configuration and resilience - Browser, MCP bridge, bot connectors, and external integrations - Platform plans and operational system docs ## Validate Links Run the documentation link check before merging documentation changes: ```bash node scripts/check-doc-links.js ``` ## Getting Started Canonical HTML: https://agentworkshq.com/docs/getting-started/ Raw Markdown: https://agentworkshq.com/docs-content/getting-started/README.md # Getting Started AgentWorks runs coding agents, model providers, browsers, and MCP tools as repeatable business workflows. This guide takes you from installation to a reviewed first run. ## 1. Install the macOS App ```bash curl -fsSL https://raw.githubusercontent.com/manishiitg/mcp-agent-builder-go/main/install.sh | bash ``` The installer downloads the latest release, installs the app, installs the local MCP bridge, clears the macOS quarantine flag, and launches AgentWorks. See the [latest release](https://github.com/manishiitg/mcp-agent-builder-go/releases/latest) for manual installation artifacts. ## 2. Complete First-Launch Setup On first launch: 1. Choose the workspace folder that will hold `workspace-docs/`. 2. Set `AUTH_SECRET`. Reuse the existing value when opening an existing workspace because it encrypts stored provider credentials. 3. Configure at least one coding agent or model provider from the app's provider settings. Provider keys are encrypted in `/config/provider-api-keys.json`. ## 3. Build the First Workflow Follow [Build Your First Workflow](first-workflow.md) to create an automation, define its operating contract, run it, and review the result. ## 4. Operate and Improve - [Schedule recurring runs](../workflow/workflow_scheduling.md) - [Monitor runs, reports, and costs](../workflow/workflow_monitoring.md) - [Understand Pulse and reporting](../workflow/self_improvement_and_reporting.md) - [Configure evidence-based Auto Improve](../workflow/auto_improvement_framework.md) ## 5. Connect the Runtime - [Configure model providers](../core/llm_configuration_and_resilience.md) - [Connect MCP tools](../core/mcp_bridge_layer.md) - [Configure browser sessions](../core/browser.md) - [Manage global and workflow secrets](../core/secrets.md) - [Connect Slack, WhatsApp, and other channels](../core/bot_connector_system.md) ## Build Your First Workflow Canonical HTML: https://agentworkshq.com/docs/getting-started/first-workflow/ Raw Markdown: https://agentworkshq.com/docs-content/getting-started/first-workflow.md # Build Your First Workflow The first workflow should be small enough to review but real enough to produce evidence. A recurring research brief, support summary, lead review, or finance check is a better first test than a large autonomous process. ## 1. Create the Automation 1. Open **Automation** mode. 2. Open **Select Automation** and choose **+ Add Automation**. 3. Enter an **Automation Name**. 4. Select an automation provider under **Agent LLM Configuration**. 5. Set the required **Workflow Folder**. 6. Choose **Save Automation**. The workflow folder becomes the durable home for the workflow definition, run artifacts, reports, learnings, and knowledge used in later runs. ## 2. Define the Operating Contract In **Chat**, describe: - the business outcome to achieve; - the inputs and systems the workflow may use; - the expected output and evidence; - the run cadence; - decisions that require human approval; - the metric or review standard that determines success. Keep the first contract explicit. For example: "Every weekday, review open support conversations, group them by urgency, cite the source messages, draft responses, and ask for approval before sending anything." ## 3. Review the Plan and Access Open **Plan** before the first run. Confirm that each step has the right worker and only the access it needs: - coding CLI or model provider; - MCP servers and tools; - browser mode; - global or workflow-scoped secrets; - human approval boundaries. See [Workflow Builder Commands and Tools](../workflow/workflow_builder_commands_and_tools.md), [MCP Bridge Layer](../core/mcp_bridge_layer.md), [Browser](../core/browser.md), and [Secrets](../core/secrets.md) for detailed configuration. ## 4. Run and Observe Start the workflow and use **Tree** for step progress or **Terminal** for the live agent session. A useful first run should leave an inspectable record rather than only a chat response. After the run, inspect: - the generated report and files; - logs and per-step execution status; - model and token cost; - any requested human input; - Pulse findings about reliability or missing evidence. See [Workflow Monitoring](../workflow/workflow_monitoring.md), [Cost and Log Measurement](../workflow/cost_and_log_measurement.md), and [Human Feedback](../workflow/human_feedback_system.md). ## 5. Improve the Next Run Fix correctness and access problems first. Then review proposed changes to instructions, scripts, evaluation criteria, or reusable skills. Approve only changes supported by run evidence and keep a human review step around consequential actions. Use [Self-Improvement and Reporting](../workflow/self_improvement_and_reporting.md), [Auto Improvement Framework](../workflow/auto_improvement_framework.md), [Evaluation System](../workflow/evaluation_system.md), and [Learning Architecture](../workflow/learning_architecture.md) to extend the loop. ## First-Run Completion Check The workflow is ready to schedule when: - the result matches the stated output contract; - source evidence is available for review; - retries and approval boundaries are clear; - cost is acceptable for the planned cadence; - the next run can reuse saved workflow context instead of starting from zero. ## Core Docs Canonical HTML: https://agentworkshq.com/docs/core/ Raw Markdown: https://agentworkshq.com/docs-content/core/README.md # Core Docs These docs describe platform subsystems that cut across workflow and multi-agent mode. ## Includes - Auth, secrets, user isolation, and policy controls - Eventing, streaming, session propagation, and runtime plumbing - LLM/provider configuration and integration docs - Browser, connector, MCP bridge, and platform plans ## Files - `azure_foundry_integration.md` - `bot_connector_system.md` - `browser.md` - `coding_agent_builder_e2e_contract.md` - `coding_agent_continuation_architecture.md` - `electron_standalone_app_plan.md` - `env-api-key-defaults.md` - `event_cleanup.md` - `event_system.md` - `folder_guard_system.md` - `llm_configuration_and_resilience.md` - `mcp_bridge_layer.md` - `mcp_session_id_propagation.md` - `multi_user_authentication.md` - `native_workspace_mode.md` - `oauth.md` - `remote_workspace_server_plan.md` - `session_and_tool_binding.md` - `secrets.md` - `skills.md` - `streaming_llm_output.md` - `tool_search_mode.md` ## Bot Connector System Canonical HTML: https://agentworkshq.com/docs/core/bot_connector_system/ Raw Markdown: https://agentworkshq.com/docs-content/core/bot_connector_system.md # Bot Connector System A platform-agnostic bot framework that allows users to interact with the agent system from messaging platforms (Slack, Discord, Telegram, WhatsApp) and the built-in web simulator. Users @mention the bot (or type in the simulator), the system starts a multi-agent session with the user's pre-configured MCP servers, skills, and tool search mode, and streams progress back to the thread. ## Architecture Overview ``` Slack / Discord / Web Simulator / ... | v BotConnector interface (per-platform) | v BotConversationManager (platform-agnostic orchestrator) | +-> buildQueryRequest() (uses user-configured servers/skills from DB) +-> startSessionInternal() (reuses handleQuery) +-> BotEventFilter -> streams progress, plan approval, human feedback to thread +-> User responds to blocking events via text in thread ``` ### Key Components | Component | File | Purpose | |-----------|------|---------| | `BotConnector` | `agent_go/cmd/server/services/bot_connector.go` | Per-platform interface | | `BotConversationManager` | `agent_go/cmd/server/services/bot_connector.go` | Platform-agnostic orchestrator | | `BotEventFilter` | `agent_go/cmd/server/services/bot_event_filter.go` | Event filter for thread updates + lifecycle | | `BotEventSubscriberAdapter` | `bot_event_adapter.go` | Bridges EventStore to BotEventSubscriber | | `WebSimulatorConnector` | `agent_go/cmd/server/services/web_simulator_connector.go` | In-memory connector for web testing | | `startSessionInternal` | `bot_session_starter.go` | Starts agent sessions programmatically | | Bot routes | `bot_routes.go` | REST API for config, sessions, history | | Simulator routes | `bot_simulator_routes.go` | REST API for the web simulator | --- ## BotConnector Interface ```go type BotConnector interface { NotificationConnector // embeds: Name(), IsEnabled(), SendNotification() SupportsThreads() bool // true for Slack, false for WhatsApp/Telegram/web_simulator StartListening(ctx context.Context) error StopListening() SendThreadMessage(ctx context.Context, threadID ThreadID, message string) (string, error) SendThreadMessageWithBlocks(ctx context.Context, threadID ThreadID, message string, blocks []MessageBlock) (string, error) UpdateMessage(ctx context.Context, threadID ThreadID, messageID string, newText string) error GetThreadHistory(ctx context.Context, threadID ThreadID) ([]ThreadMessage, error) SetMessageHandler(handler BotMessageHandler) SetInteractionHandler(handler BotInteractionHandler) GetFormatter() MessageFormatter } ``` Each platform implements this interface. The `WebSimulatorConnector` stores messages in-memory for the frontend to poll. --- ## Session Start (Direct — No Analysis) When a user sends a message, the system starts a multi-agent session directly using the user's pre-configured capabilities. There is no intermediate analysis LLM step. `buildQueryRequest()` constructs the session from the `_global` bot connector config stored in the DB: - **MCP Servers**: from `default_servers` in the `_global` config - **Skills**: from `default_skills`, falling back to all discovered skills - **Tool search mode**: enabled automatically when >2 servers are configured - **Delegation mode**: always `"plan"` (multi-agent chat) - **Workspace access**: always enabled - **Provider/model**: high tier from `delegation_tier_config` (DB → env → defaults) - **API keys**: from `provider_api_keys` in the `_global` config - **User secrets**: loaded from server-side encrypted storage --- ## End-to-End Flows ### Flow 1: Web Simulator ``` 1. User types "list my google sheets" in simulator 2. HandleMessageSync(): a) No active session → buildQueryRequest(query) b) Create bot_session (status: "running") c) Start multi-agent session in background d) Return: { type: "follow_up", thread_offset: N } 3. Frontend polls for messages, sees progress: "[Sub-agent] Planning task..." "**Plan ready for review:** ..." 4. User types "approve" 5. HandleMessageSync(): a) Session running, awaitingUserInput = true, blockingEventType = "plan_approval" b) isPlanApprovalResponse("approve") → true c) Inject follow-up: "Approved. Execute the plan." 6. Plan executes, sub-agents run, progress streams to thread 7. Session completes: "Session completed." ``` ### Flow 2: Slack @mention ``` 1. User @mentions bot in #general 2. HandleIncomingMessage() → startNewSessionDirect() 3. Posts "Starting session... (tag me to follow up in this thread)" to thread 4. buildQueryRequest() → startSessionInternal() 5. Events stream to thread via BotEventFilter 6. Blocking events (plan approval, human feedback) shown in thread 7. User @mentions bot in thread → follow-up injected 8. Session completes → removed from active sessions map 9. User @mentions bot again → starts a fresh session (with thread history for context) ``` ### @Mention Requirement The bot **only responds to @mentions** — plain thread replies are ignored unless the session is in a blocking state (plan approval, human feedback). This prevents: - Other users chatting in the thread from accidentally triggering the bot - Duplicate message processing (Slack sends both a `MessageEvent` and `AppMentionEvent` for @mentions in threads) `handleSocketModeMessage` skips messages containing the bot's `<@BOT_ID>` to avoid double-processing with `handleAppMentionEvent`. The `IsMention` flag on `BotIncomingMessage` controls whether `handleExistingSession` allows follow-up injection. ### Follow-Up Architecture Each user message (initial or follow-up) creates a **new agent** via `handleQuery` — the same pattern as the regular chat UI. Conversation history from the DB provides continuity between turns. Follow-ups use `buildQueryRequest(query)` to construct the full config, ensuring identical settings (servers, skills, delegation mode, API keys, secrets) as the initial session. The `SessionFollowUpFunc` accepts the complete `reqMap` and `sendFollowUpInternal` simply marshals and forwards it to `handleQuery`. ### Session Cleanup When `runSession` completes (event filter signals done or context cancelled), the session is: 1. Marked as `completed` in DB 2. **Removed from the in-memory `m.sessions` map** This ensures subsequent messages don't find a stale entry with a dead event filter. Instead, the next @mention triggers a DB lookup, finds the completed session, and starts a fresh session via `startNewSessionDirect` — which creates a new event filter that properly forwards responses to the thread. ### Flow 3: Blocking Events (Generic) All blocking events follow the same pattern: ``` Agent emits blocking event → BotEventFilter: 1. Format event as thread message 2. Set active.awaitingUserInput = true 3. Set active.blockingEventType = event type User responds in thread → HandleMessageSync / handleExistingSession: 1. Check blockingEventType 2. For plan_approval: translate "approve" → "Approved. Execute the plan." 3. For human_feedback: forward user's text as follow-up 4. buildQueryRequest(response) → sendFollowUpInternal(reqMap) 5. Clear awaitingUserInput ``` | Blocking Event | Thread Message | User Response | |---|---|---| | `plan_approval` | Plan markdown + "Reply **approve** or **reject**" | "approve" / "reject" / feedback text | | `blocking_human_feedback` | Question/prompt from agent | Free text answer | --- ## SyncMessageResult (Web Simulator) The `HandleMessageSync` method returns a synchronous result for the web simulator: ```go type SyncMessageResult struct { Type string `json:"type"` // "conversation" or "follow_up" Response string `json:"response,omitempty"` // text reply (conversation only) ThreadID string `json:"thread_id"` SessionID string `json:"session_id,omitempty"` // internal chat session ID BotSessionID string `json:"bot_session_id,omitempty"` // set when awaiting confirmation ThreadOffset int `json:"thread_offset,omitempty"` // message count for polling init } ``` - **`follow_up`**: Session is running. Frontend polls `/api/simulator/messages?thread_id=X&since_index=N` for updates. --- ## Event Filter **File**: `agent_go/cmd/server/services/bot_event_filter.go` The event filter subscribes to session events and forwards filtered updates to the thread. It also manages session lifecycle by tracking blocking events and delegations. ### Events Handled | Event Type | Action | |---|---| | `delegation_start` | Track sub-agent name, increment pending count, send "**Starting:** Sub-agent (model)" to thread | | `delegation_end` | Decrement pending delegation count, check if session is done | | `llm_generation_end` | Show main agent text responses (HierarchyLevel 0 only, skips sub-agents and pure tool-call turns) | | `unified_completion` | Format with sub-agent name + result + turns/duration stats (main-level triggers session done check) | | `plan_approval` | Show plan content + approval instructions, set blocking state | | `blocking_human_feedback` | Show feedback question, set blocking state | | `agent_error` / `conversation_error` | Show error message | ### Sub-Agent Name Tracking `trackDelegationName()` extracts the instruction from `delegation_start` events and stores a short name (first line, capped at 60 characters) keyed by `correlation_id`. This name is used later in `unified_completion` formatting to label sub-agent results instead of showing the full instruction text. ### Session Lifecycle The event filter tracks: - **Pending delegations**: increment on `delegation_start`, decrement on `delegation_end` - **Blocking state**: set on any blocking event, cleared when user responds - **Completion received**: set when a main-level (HierarchyLevel 0) `unified_completion`, `agent_end`, or `conversation_end` arrives The session is "done" when all three conditions are met: completion received, no pending delegations, and no blocking state. The `sessionDone` flag ensures the callback fires at most once. This triggers "Session completed." and DB status update. ### Message Formatting ``` delegation_start: "**Starting:** Sub-agent (model-name)" llm_generation_end (main agent only, level 0): "**Agent:** {full LLM text content}" unified_completion — Main agent (HierarchyLevel 0): "**Result:** {full final_result} _N turns, Xs_" unified_completion — Sub-agent (HierarchyLevel > 0): "**[{short name from trackDelegationName}]** {full final_result} _N turns, Xs_" ``` Note: Full response text is shown without truncation. Meta stats (turns/duration) are only shown when they have meaningful values. ### Workspace Path → Shareable URL The event filter automatically converts workspace file paths in outgoing messages to clickable shareable URLs. This works in two passes: 1. **Markdown links**: a relative report link is converted to `https://app.example.com/file?path=&uid=default`. 2. **Bare paths**: `Chats/xxx/report.md` is converted to a shareable link with the same file endpoint. Matches paths starting with `Chats/` or `Downloads/` that contain at least one subfolder and a file extension. **Requires**: `PUBLIC_URL` environment variable set to the app's public base URL. Without it, paths are left as-is. --- ## Bot Session Status Flow ``` running ←→ awaiting_user_input → completed | | +→ failed ←————————————————————————+ ``` | Status | Description | |---|---| | `awaiting_plan_approval` | Plan created, waiting for user to approve/reject | | `running` | Agent session actively processing | | `completed` | Session finished successfully | | `failed` | Session failed or was cancelled | --- ## Bot Configuration (Global) **Files**: `frontend/src/components/settings/BotConfigModal.tsx`, `frontend/src/components/sidebar/HumanFeedbackConnectorsSection.tsx` Bot capabilities (MCP servers and skills) are configured globally via a standalone **Bot Configuration** modal, accessible from the sidebar's Human Feedback Connectors section. This configuration applies to **all bot interfaces** (Slack, Web Simulator, etc.), not just the simulator. ### UI - **Sidebar**: Top-level "Bot Configuration" card with a "Configure" button — separate from individual connector cards - **Modal**: Reuses the same `ServerSelectionDropdown` and `SkillSelectionDropdown` components used in the chat input area - **Tier display**: Read-only bar showing delegation tier models (fetched from DB config, falls back to LLM store) - **Save**: Persists selected servers/skills to DB via `POST /api/bot/simulate/config` (`default_servers`, `default_skills`) ### Data Flow 1. On open: fetches saved config from `GET /api/bot/simulate/config`, available servers from `useMCPStore` 2. User selects servers/skills using the standard dropdowns 3. On save: `POST /api/bot/simulate/config` with `{ default_servers, default_skills }` 4. When any bot session starts, `buildQueryRequest()` uses `default_servers`/`default_skills` from the `_global` config 5. Tool search mode is auto-enabled when >2 servers are configured --- ## Web Simulator **Files**: `agent_go/cmd/server/services/web_simulator_connector.go`, `bot_simulator_routes.go`, `frontend/src/components/settings/BotSimulatorModal.tsx` The web simulator provides a chat-like UI for testing the bot flow without a Slack workspace. It uses the global bot configuration for server/skill selection. ### Architecture - `WebSimulatorConnector` implements `BotConnector` with in-memory thread storage - Messages stored per-thread in `[]SimulatorMessage` - Frontend polls `/api/simulator/messages` for new messages using a `setTimeout` chain (not `setInterval`) to prevent overlapping polls when API calls are slow - Message ID deduplication in the frontend prevents duplicate messages from race conditions - `threadOffsetRef` tracks where polling starts to avoid re-fetching ### REST API | Method | Path | Description | |---|---|---| | `POST` | `/api/simulator/send` | Send user message, returns `SyncMessageResult` | | `GET` | `/api/simulator/messages` | Poll for messages (`?thread_id=&since_index=`) | | `GET` | `/api/simulator/threads` | List all threads | | `POST` | `/api/simulator/reset` | Clear all threads and sessions | | `GET` | `/api/simulator/mode` | Get current thread mode | | `POST` | `/api/simulator/mode` | Set thread mode (threaded/non-threaded) | ### Delegation Tier Config Sync On modal open, the frontend syncs its `delegationTierConfig` (from `useLLMStore`) to the DB via `PUT /api/bot/connectors/_global`. This includes: - `delegation_tier_config`: high/medium/low tier provider/model - `provider_api_keys`: API keys per provider The agent session uses the high tier as the main provider. --- ## Session Configuration ### Query Request Building `buildQueryRequest()` constructs the session config from the `_global` bot connector config: | Field | Source | |---|---| | `query` | Original user message | | `provider` / `model_id` | High tier from delegation config (DB → env → defaults) | | `servers` | `default_servers` from `_global` config | | `selected_skills` | `default_skills` from `_global` config (falls back to all discovered) | | `use_tool_search_mode` | `true` when >2 servers configured | | `delegation_tier_config` | From DB `_global` config or env vars | | `llm_config.primary` | Same as `provider`/`model_id` (ensures follow-ups recover correct model) | | `llm_config.api_keys` | From DB `provider_api_keys` | | `decrypted_secrets` | Loaded from server-side encrypted storage | ### User Secrets Bot sessions load server-side stored secrets via `UserSecretsLoaderFunc`. These are encrypted in the DB and decrypted at session start, injected as `decrypted_secrets` in the query request. --- ## Text-Based Response Detection ### Plan Approval Responses ```go // Approve: approve, approved, execute, go, yes, y, ok, proceed, do it, run it, start, lgtm // Reject: reject, rejected, no, n, cancel, stop, nope, nah, abort // Other text: forwarded as feedback to the agent ``` ### Session End Commands (thread-less platforms) ```go // End: done, end, stop, reset, new session, quit, exit ``` --- ## Database Schema **Migration**: `pkg/database/migrations/019_add_bot_connector_tables.sql` ### bot_connector_config | Column | Type | Description | |---|---|---| | `id` | TEXT PK | Platform name or `"_global"` for shared config | | `enabled` | BOOLEAN | Whether the connector is enabled | | `bot_mode` | BOOLEAN | Full bot vs notification-only | | `config_json` | TEXT | Platform-specific config + shared config (tier, API keys) | | `auto_confirm` | BOOLEAN | Skip confirmation step | | `allowed_channels` | TEXT | JSON array of allowed channel IDs | ### bot_sessions | Column | Type | Description | |---|---|---| | `id` | TEXT PK | UUID | | `platform` | TEXT | "slack", "web_simulator", etc. | | `channel_id` | TEXT | Platform channel ID | | `thread_ts` | TEXT | Thread root timestamp | | `session_id` | TEXT | Internal chat session ID | | `user_id` | TEXT | Platform user ID | | `query` | TEXT | Original user query | | `status` | TEXT | See status flow above | | `config_json` | TEXT | Thread context JSON | ### bot_messages | Column | Type | Description | |---|---|---| | `id` | TEXT PK | UUID | | `bot_session_id` | TEXT FK | References bot_sessions(id) | | `direction` | TEXT | "incoming" / "outgoing" | | `message_type` | TEXT | progress / conversation / confirmation | | `content` | TEXT | Message content | | `platform_message_id` | TEXT | For message updates | --- ## Import Cycle Avoidance The `services` package cannot import `internal/events`. Solved with: 1. **`BotEventSubscriber` interface** (`agent_go/cmd/server/services/bot_connector.go`): abstracts `SubscribeBot(sessionID) -> (chan, unsubscribe)` 2. **`BotEventSubscriberAdapter`** (`bot_event_adapter.go`): bridges `EventStore` to `BotEventSubscriber` 3. **`SessionStartFunc`**: callback for starting new agent sessions 4. **`SessionFollowUpFunc`**: callback for injecting follow-ups — accepts full `reqMap map[string]interface{}` (built by `buildQueryRequest()`) so follow-ups get identical config (servers, skills, delegation mode, API keys, secrets) as initial sessions 5. **`UserSecretsLoaderFunc`**: callback for loading decrypted user secrets All wired in `server.go` during startup. --- ## Adding a New Platform 1. **Create `services/{platform}_connector.go`** implementing `BotConnector` 2. **Implement `MessageFormatter`** for the platform's markup 3. **Register in `server.go`**: ```go botManager.RegisterConnector(platformService) ``` 4. **Add `bot_connector_config` row** for the platform The `BotConversationManager` handles session management, event filtering, and blocking event routing identically across all platforms. --- ## Access Control ### allowed_emails The `_global` bot connector config supports an `allowed_emails` array. When set, only users whose email matches (case-insensitive) can use the bot. Rejected users receive a message: "Sorry, you don't have access to use this bot. Please contact your administrator to get access." Email resolution is platform-specific (e.g., Slack's `users.info` API via `resolveUserEmail`). ## Browser System Canonical HTML: https://agentworkshq.com/docs/core/browser/ Raw Markdown: https://agentworkshq.com/docs-content/core/browser.md # Browser System Complete reference for browser automation in mcp-agent-builder-go — session limits, CDP local browser, Playwright artifacts, and known bugs. --- ## Table of Contents - [Session Limit Manager](#session-limit-manager) - [CDP Local Browser Connection](#cdp-local-browser-connection) - [Playwright Artifacts and Output Location](#playwright-artifacts-and-output-location) - [Browser Session Identity Split Plan](#browser-session-identity-split-plan) - [Known Bugs](#known-bugs) --- ## Session Limit Manager The session tracker prevents unbounded browser process growth by enforcing per-workflow and global limits. **Source:** `agent_go/pkg/browser/session_tracker.go` ### Limits | Limit | Value | Enforcement | |-------|-------|-------------| | Per workflow/chat | `MaxBrowserSessionsPerChat = 1` | Returns error to LLM — must close existing session first | | Global (all sessions) | `MaxBrowserSessionsGlobal = 8` | Auto-evicts oldest (LRU) session | ### How It Works 1. **Registration:** When the LLM calls `agent_browser(command="open", ...)`, the executor extracts the `chatSessionID` from context and calls `tracker.CheckLimits()`. 2. **Per-chat check:** If the workflow already has 1 active session, returns an error: ``` ERROR: Cannot open browser session "" — you already have 1 active browser sessions (max 1 per workflow). Active sessions: [...]. Close one first using agent_browser(command="close", session="") before opening a new one. ``` 3. **Global check:** If 8+ sessions exist globally, the oldest (least-recently-used) session is auto-closed. 4. **Reuse:** If the named session already exists in the tracker, it's a reuse — always allowed. 5. **Touch:** Every `agent_browser` call updates the session's `lastUsed` timestamp. ### Session Lifecycle ``` 1. Workflow starts └─ Browser tool executor created with chatSessionID in context 2. LLM calls agent_browser(command="open", session="my_session") └─ CheckLimits() validates per-chat (< 1) and global (< 8) └─ Touch() registers session with timestamps 3. During workflow └─ Each agent_browser call updates lastUsed via Touch() 4. Workflow ends (stop/clear/completion) └─ CloseAllForChat(sessionID) closes all browser processes └─ RemoveAllForChat() removes tracker entries 5. Server restart └─ In-memory tracker cleared └─ Kill-all sent to workspace-api for orphaned chromium processes ``` ### Tracking Data Structure ```go type browserSessionInfo struct { browserSession string // e.g., "twitter_research" chatSessionID string // owning workflow/chat session lastUsed time.Time createdAt time.Time } ``` ### Key Functions | Function | Purpose | |----------|---------| | `Touch(browserSession, chatSessionID)` | Register or update last-used time | | `CheckLimits(browserSession, chatSessionID)` | Validate per-chat and global limits | | `CountForChat(chatSessionID)` | Count active sessions for a workflow | | `SessionsForChat(chatSessionID)` | List browser session names for a workflow | | `GetOldestSession()` | Find LRU session globally (for auto-eviction) | | `GetOldestSessionForChat(chatSessionID)` | Find LRU session for a specific workflow | | `RemoveAllForChat(chatSessionID)` | Remove all tracker entries for a workflow | | `CloseAllForChat(chatSessionID, client)` | Close browser processes + remove entries | | `Clear()` | Remove all tracked sessions (server restart) | ### Key Files | File | Role | |------|------| | `agent_go/pkg/browser/session_tracker.go` | Core tracker with limits | | `agent_go/pkg/browser/executor.go` | Limit check + enforcement on each command | | `agent_go/cmd/server/server.go` | Cleanup on workflow stop (`cleanupBrowserSessions`) | | `agent_go/cmd/server/virtual-tools/workspace_browser_tools.go` | Injects chatSessionID into context | | `workspace/handlers/browser_session_tracker.go` | Workspace-side tracker (code execution mode) | ### Frontend Monitoring `frontend/src/components/workspace/BrowserProcesses.tsx` displays: - Active sessions grouped by process - Age, idle time, CPU/memory usage - Buttons to kill individual sessions or cleanup all - Orphaned session detection API endpoints: `/api/browser/sessions` (agent_go), `/api/browser/processes` (workspace-api). --- ## CDP Local Browser Connection Connect the agent's browser tools to your real Chrome browser via Chrome DevTools Protocol (CDP), allowing you to watch the agent navigate in real-time and reuse your logged-in sessions. ### Overview By default, browser-based MCP servers (like `playwright` or `agent-browser`) run an isolated, headless browser inside a container. By using **CDP mode**, you can point these tools to a Chrome instance running on your host machine. **Foreground behavior:** CDP is connected to a visible real Chrome window. Browser actions may bring Chrome to the foreground and steal keyboard focus from the user. This is expected for shared visible Chrome and is separate from tab isolation. Use headless mode for background-safe runs, or run schedules against a dedicated automation Chrome profile/port instead of the user's primary Chrome. ### How it Works 1. **Launch Chrome with Remote Debugging**: You start Chrome on your host with a specific port (default `9222`). 2. **Connection String**: The agent is given a CDP URL (e.g., `http://host.docker.internal:9222`). 3. **Connectivity Check**: The frontend verifies the connection before starting the session. 4. **Shared browser session**: In CDP mode, `agent_browser` commands are remapped to a shared raw agent-browser session per CDP port, e.g. `shared-cdp-9222`. This lets multiple workflows reuse the same Chrome while still forcing each command to name the tab it intends to use. ### Shared CDP Tabs Native agent-browser tracks an active tab per `--session`. In a shared workflow environment that is too implicit: whichever workflow last selected a tab can influence the next page action. The project wrapper therefore requires an explicit tab for CDP work. Use the `tab` command to choose/create the tab, call `open` with URL-only args, then include an inline tab argument on later page actions. Allowed page action forms: ```json {"command": "tab", "args": ["profile"], "session": "workflow_a"} {"command": "open", "args": ["https://example.com"], "session": "workflow_a"} {"command": "snapshot", "args": ["tab", "profile", "-i"], "session": "workflow_a"} {"command": "click", "args": ["--tab", "profile", "@e1"], "session": "workflow_a"} {"command": "eval", "args": ["tab", "profile", "document.title"], "session": "workflow_a"} ``` If a CDP page action omits the tab, the tool returns an error with the selected-tab hint for the current workflow. The LLM is expected to reuse that selected tab or create a labeled tab, then retry the command. `open` is the exception: it uses the workflow's previously selected tab and passes only the URL to agent-browser. Tab management commands: ```json {"command": "tab", "args": [], "session": "workflow_a"} {"command": "tab", "args": ["new", "--label", "profile", "https://example.com"], "session": "workflow_a"} {"command": "tab", "args": ["profile"], "session": "workflow_a"} ``` Operational rules: - Try the compact real tab list first and reuse a matching tab when CDP responds; if it times out, use the selected-tab fallback. - Create one stable labeled tab only when no tab is selected or the selected tab is unrelated. - Do not close user tabs unless explicitly requested. - Do not rely on "latest tab" or session active-tab state for page actions. - The wrapper serializes `select tab -> action` with a per-CDP-port mutex. Two page commands on the same CDP port do not interleave; different CDP ports are independent. ### Configuration #### Launch Chrome (Host) **macOS:** ```bash /Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --remote-debugging-port=9222 ``` **Windows:** ```cmd "C:\Program Files\Google\Chrome\Application\chrome.exe" --remote-debugging-port=9222 ``` #### Connection Logic The system uses a two-tier connectivity check (`frontend/src/services/api.ts` and `agentApi.checkCdpPort`): 1. **Agent API Check**: Calls `agent_go`'s `/api/cdp-check` — TCP dial from agent server to the port. 2. **Workspace API Check**: If running in Docker, also tries the Workspace API's `/api/cdp-check` — critical because browser tools execute inside the workspace container (different network view). ### Frontend UI Features - **Check Connection Button**: Found in Preset Modal and Chat Input settings, with immediate feedback. - **Auto-Check**: Debounced connection check as you type the port number. - **macOS Helper**: Download link for a pre-configured macOS launcher. ### macOS "Damaged Package" Fix ```bash xattr -cr /path/to/Chrome-CDP.app ``` ### Network Paths (Docker) - **macOS/Windows**: `http://host.docker.internal:9222` - **Linux**: `http://172.17.0.1:9222` (or your host IP) ### Troubleshooting - **Connection Refused**: Ensure Chrome is running with `--remote-debugging-port=9222`. Check no other process uses port 9222 (`lsof -i :9222`). Close all other Chrome instances first. - **Agent sees a blank page**: CDP only allows one connection per tab. If DevTools "Inspect" window is open for that tab, the agent's tools may be blocked. - **Missing tab error**: In shared CDP mode, retry with `["tab", "", ...]` or `["--tab", "", ...]` in `args`. The error includes the selected-tab hint, not a full browser-wide tab dump. - **Wrong tab acted on**: Check for direct CDP code or shell scripts that bypass `agent_browser`. Raw CDP scripts must use `/json/list`, connect to the chosen target, and avoid navigation/actions plus `Target.createTarget` / `Target.closeTarget` unless disposable raw-CDP control is explicitly required and the user accepts that it bypasses shared-browser locking. --- ## Playwright Artifacts and Output Location ### The Problem By default, `@playwright/mcp` might save artifacts in the process cwd. In containerized environments, files get lost in temporary directories or scattered across the repository root. ### The Solution: `working_dir` Injection The project uses a custom MCP client that supports a `working_dir` property in the server configuration. #### Configuration (`agent_go/configs/mcp_servers_clean.json`) ```json "playwright": { "command": "npx", "args": [ "@playwright/mcp@latest", "--output-dir", "../workspace-docs/Downloads", "--isolated" ], "working_dir": "../workspace-docs/Downloads" } ``` **Key Parameters:** - **`working_dir`**: Sets the OS-level cwd for the spawned `npx` process. - **`--output-dir`**: Tells Playwright where to save snapshots and traces. - **`--isolated`**: Prevents browser data (cookies, storage) from leaking between sessions. ### Benefits 1. **Visibility**: Artifacts land in `workspace-docs/Downloads`, indexed by semantic search and `list_directory`. 2. **Persistence**: Files survive session restarts (persistent `workspace-docs` volume). 3. **Cleanliness**: Prevents repo root from being cluttered with screenshot/download files. ### Interaction with CDP Mode Even when controlling your local browser via CDP, `working_dir` injection is active — downloads are still routed to the designated workspace folder. ### Troubleshooting - **"File not found" after download**: Check the `working_dir` path in MCP config. If relative, it's relative to the `agent_go` directory. - **Duplicate filenames**: Playwright appends timestamp/UUID to screenshots. For specific filenames, use `take_screenshot` with a custom path. --- ## Browser Session Identity Split Plan ### Problem A single MCP session ID currently does two jobs: 1. **Tool session identity** — session-scoped `MCP_API_URL`, custom tool routing, code execution mode HTTP calls. 2. **Browser session identity** — Playwright / `agent_browser` browser reuse, page state continuity, login persistence. This coupling breaks in the workflow builder. ### Concrete Failure: Builder + `run_saved_main_py` - `run_saved_main_py(step_id, group_id)` executes through the workshop controller in a **group MCP session**. - The builder chat agent uses its own **chat session**. - Result: builder cannot inspect the browser opened by the workflow step because the session IDs differ. ### Current `share_browser` Behavior Available on `call_sub_agent()`, `call_generic_agent()`, and `delegate()`: - `share_browser=true` (default): Child keeps parent's MCP session → browser state shared. - `share_browser=false`: Isolated MCP session ID → browser isolated, but **also changes tool routing** (undesirable side-effect). ### Goal: Separate the Two Identities Each agent/session should have: - **`tool_session_id`** — MCP/custom tool routing, `MCP_API_URL`, stable per chat/workflow agent. - **`browser_session_id`** — browser reuse only, can be shared across agents when desired. ### Proposed Browser Session Key For workflow builder: `browser::::` (canonical `group_id`, not display name). ### Desired Behavior After Split - **Workflow builder**: Builder keeps its own `tool_session_id`. `run_saved_main_py` publishes the active `browser_session_id`. Subsequent builder browser inspection uses that ID. - **Sub-agents**: `share_browser=false` only creates a new browser session, not a new tool session. - **Multi-agent chat**: Shared browser is opt-in, not default. ### What Must Change 1. **Session model**: Introduce `ToolSessionID` + `BrowserSessionID` (fallback to tool session if browser session empty). 2. **Browser tools**: Playwright and `agent_browser` must resolve `browser_session_id` first, fall back to `tool_session_id`. 3. **Code execution env**: Add `MCP_BROWSER_SESSION_ID` or equivalent, keep `MCP_API_URL` on tool session. 4. **Workshop controller state**: Track `map[groupID]browserSessionID` and optional `lastActiveGroupID`. 5. **Cleanup lifecycle**: Closing a tool session must not auto-destroy a shared browser session. ### Rollout Phases 1. **Phase 1**: Internal split with compatibility fallback (browser falls back to tool session). 2. **Phase 2**: Workflow builder/workshop adoption (`run_saved_main_py`, `execute_step`, builder inspection). 3. **Phase 3**: Delegation adoption (`share_browser=false` isolates browser only). 4. **Phase 4**: General multi-agent adoption (shared browser opt-in). ### Files Involved **mcp-agent-builder-go:** - `agent_go/pkg/orchestrator/base_orchestrator.go` — single-session propagation - `agent_go/pkg/orchestrator/agents/workflow/step_based_workflow/controller.go` — workshop group session cache - `agent_go/pkg/orchestrator/agents/workflow/step_based_workflow/controller_workshop.go` — workshop group switching - `agent_go/pkg/orchestrator/agents/workflow/step_based_workflow/controller_agent_factory.go` — agent config overrides, `share_browser` handling - `agent_go/pkg/orchestrator/agents/workflow/step_based_workflow/planning_exports.go` — workshop session setup - `agent_go/cmd/server/server.go` — workflow-phase chat agent creation, session-aware executors **mcpagent:** Browser session registry/reuse logic, Playwright session lookup, `agent_browser` execution path. ### Risk If browser sessions are shared too broadly, two agents may interfere with the same page. Shared browser reuse should be explicit, scoped, and default-on only where already expected (parent/sub-agent in same task). --- ## Known Bugs ### Playwright: "transport error: transport closed" **Status:** Known / Upstream **Symptom:** `failed to call tool browser_close: transport error: transport closed` **Meaning:** The MCP connection (stdio pipe to `npx @playwright/mcp`) is already closed by the time the call runs. Not that `browser_close` is invalid — the transport is gone. **Typical causes:** 1. Playwright MCP process exited (crash, OOM, uncaught exception). 2. Connection was closed on our side (`CloseSession` called, workflow ended). 3. Browser/process died earlier — MCP server closes transport or exits. 4. Timeout or kill (subprocess killed, pipes close). **Fix implemented:** "transport closed" is now treated as a broken-pipe error: - `mcpclient.IsBrokenPipeError` returns true for `"transport closed"`. - Agent path: broken-pipe handler closes old client, gets fresh connection, retries once. - HTTP executor path: same — fresh connection and one retry. - Registry: new agents calling `GetOrCreateConnection` ping existing connection; if dead, it's replaced. **When it still happens:** If retry also fails or the new process dies again. Start a new workflow/session. **What to do:** - If cleaning up after browser close: treat as "connection already gone", continue. - If you need a fresh session: start a new workflow run / chat session. - If happening often mid-workflow: check for Playwright/subprocess crashes, OOM, or premature `CloseSession`. ### Playwright: Screenshots/Snapshots Saved to Repo Root **Status:** Fixed **Problem:** With `--output-dir` set, custom filenames (e.g., `screenshot.png`) were written to process root instead of the configured directory. Auto-generated filenames worked correctly. **Root cause:** 1. Upstream Playwright MCP resolves custom filenames relative to process cwd, not `--output-dir` ([playwright-mcp#1390](https://github.com/microsoft/playwright-mcp/issues/1390)). 2. No working-directory support when spawning the MCP subprocess. 3. Session registry reuses connections by `(sessionID, serverName)` only — config not in key. **Fix:** 1. Added `MCPServerConfig.WorkingDir` and `RuntimeConfigOverride.WorkingDir` to mcpagent. 2. `StdioManager` starts subprocess with `cmd.Dir = workingDir`. 3. Workflow override (`setupBrowserDownloadsPathOverride`) sets both `--output-dir` and `WorkingDir` to the run's `execution/Downloads`. 4. `.gitignore` fallback for any artifacts that still land at repo root. **Files:** `mcpagent/mcpclient/config.go`, `mcpagent/mcpclient/stdio_manager.go`, `mcpagent/mcpclient/client.go`, `agent_go/pkg/orchestrator/agents/workflow/step_based_workflow/controller_agent_factory.go`. ### Related Issues - **"Browser is already in use"** → use `--isolated` in Playwright MCP config. - **Session not found (HTTP transport)** → applies to streamable-http; stdio uses "transport closed" as the equivalent. - Upstream refs: [playwright-mcp#1245](https://github.com/microsoft/playwright-mcp/issues/1245), [#1307](https://github.com/microsoft/playwright-mcp/issues/1307), [#1140](https://github.com/microsoft/playwright-mcp/issues/1140), [#1390](https://github.com/microsoft/playwright-mcp/issues/1390). ## LLM Configuration & Resilience Canonical HTML: https://agentworkshq.com/docs/core/llm_configuration_and_resilience/ Raw Markdown: https://agentworkshq.com/docs-content/core/llm_configuration_and_resilience.md # LLM Configuration & Resilience This document outlines the system for managing LLM configurations, user-driven fallbacks, and the automated temporary LLM cascading flow for execution resilience. --- ## 1. User-Driven LLM Configuration **Status**: Implementation Complete (2026-01-05) ### 📋 Overview The Model Library combines provider discovery, setup status, available models, and reusable saved configurations. Workflows and Chief of Staff select models directly; saved configurations are shortcuts, not a required publishing layer. Provider authentication is stored separately in workspace provider-key config. **Key Principles:** - **Simple mode**: Stores a coding-agent provider profile. Current role defaults resolve from `multi-llm-provider-go` after every app update. - **Advanced mode**: Stores direct provider/model/options/fallback selections for each role. - **Saved configurations**: Optional reusable provider/model/options combinations. - **Separate Auth**: Model metadata does not store secrets; provider auth is managed separately. - **Backend Defaults**: Backend uses ambient credentials (AWS Bedrock) for internal operations only. - **Fallback Chain**: Simple ordered fallback array configured by the user. ### 📁 Key Files & Locations | Component | File | Key Functions | |-----------|------|---------------| | **LLM Store** | `frontend/src/stores/useLLMStore.ts` | `savedLLMs`, `refreshAvailableLLMs()`, `getCurrentLLMOption()` | | **Model Library** | `frontend/src/components/llm/LibraryTab.tsx` | Provider readiness and saved configurations | | **Fallbacks Tab** | `frontend/src/components/llm/FallbacksTab.tsx` | Primary selection, fallback chain | | **LLM Dropdown** | `frontend/src/components/LLMSelectionDropdown.tsx` | Rich metadata display | | **LLM Types** | `frontend/src/types/llm.ts` | `LLMOption` interface | | **API Types** | `frontend/src/services/api-types.ts` | `SavedLLM`, `LLMModel`, `AgentLLMConfiguration` | | **Backend Server** | `agent_go/cmd/server/server.go` | No internal LLM required | ### 🔄 Workflow ``` User opens the unified Model Library ↓ Configures provider authentication when required ↓ Simple: selects a provider profile; Advanced: selects models directly ↓ Optional: saves a provider/model/options combination for reuse ↓ Agent execution resolves the role config plus provider auth ``` ### 🏗️ Architecture #### Data Flow | Step | Frontend | Backend | |------|----------|---------| | 1. Configure | Provider tabs (OpenRouter, Bedrock, etc.) | - | | 2. Select | Provider profile or explicit role config | Validates supported provider/models | | 3. Reuse | Optional `saveLLM()` → `savedLLMs[]` | - | | 4. Execute | Sends `LLMConfig` with API key | Uses provided credentials | #### Types ```typescript // Optional saved configuration interface SavedLLM extends LLMModel { id: string name: string options?: Record // reasoning_effort, thinking_level, etc. } // Agent configuration sent to backend interface AgentLLMConfiguration { primary: LLMModel // Selected directly or resolved from a provider profile fallbacks: LLMModel[] // Ordered fallback chain } ``` ### ⚙️ Configuration #### Backend Defaults (run_server_with_logging.sh) | Variable | Value | Purpose | |----------|-------|---------| | `DEEP_SEARCH_MAIN_LLM_PROVIDER` | `bedrock` | Uses AWS credentials (no API key) | | `DEEP_SEARCH_MAIN_LLM_MODEL` | `global.anthropic.claude-sonnet-4-5-*` | Default model | | `AGENT_PROVIDER` | `bedrock` | Internal operations only | **Note:** Backend no longer creates an `internalLLM` at startup. Agent execution uses the selected workflow or Chief of Staff role config. ### 🛠️ UI Components | Component | Purpose | Key Features | |-----------|---------|--------------| | **Model Library** | Providers plus saved configs | Readiness, models, tier defaults, reusable configurations | | **Fallbacks Tab** | Configure fallback chain | Add a direct model or reusable saved configuration | | **LLM Dropdown** | Select LLM for execution | Rich metadata (cost, context, reasoning options) | #### LLM Dropdown Display ``` ┌─────────────────────────────────────────┐ │ OPENROUTER │ │ My GPT-4o Config │ │ gpt-4o │ │ 📦 128k 💲$2.50/1M 🌡️ 0.7 │ │ Reasoning: medium │ ├─────────────────────────────────────────┤ │ BEDROCK │ │ Production Claude │ │ claude-sonnet-4.5 │ │ 📦 200k 💲$3.00/1M 🌡️ 0.0 │ └─────────────────────────────────────────┘ ``` ### 🔍 For LLMs: Quick Reference **Constraints:** - ✅ Workflows can select provider/model/options directly - ✅ Simple profiles receive new provider defaults after an app update - ✅ Saved configurations are optional reusable shortcuts - ✅ Backend uses Bedrock (AWS credentials) for internal ops - ❌ No hardcoded API keys in backend - ❌ No `internalLLM` created at startup **Key Store Actions:** ```typescript // Save current config for reuse saveLLM(llm, name, modelName, authMethod) // Provider/profile data comes from the backend manifest loadProviderManifest() ``` **Auto-refresh triggers:** - `saveLLM()` → calls `refreshAvailableLLMs()` - `deleteSavedLLM()` → calls `refreshAvailableLLMs()` - `loadDefaults()` → calls `refreshAvailableLLMs()` --- ## 2. Execution Model Selection ### 📋 Overview Execution agents now select models from a fixed priority chain: step override, sub-agent override, tiered resolution, then workflow fallback. Retry state still matters for control flow, but it no longer changes the execution model. **Key Benefits:** - **Explicit overrides**: Step `execution_llm` wins whenever it is set - **Predictable tiering**: Tiered mode resolves only when no higher-priority override exists - **Stable retries**: Retry handling is separate from model selection ### 📁 Key Files & Locations | Component | File Path | Key Functions | |-----------|-----------|---------------| | **Retry Logic** | [`agent_go/pkg/orchestrator/agents/workflow/step_based_workflow/controller_execution.go`](../../agent_go/pkg/orchestrator/agents/workflow/step_based_workflow/controller_execution.go) | `isRetryAfterValidationFailure` calculation (lines 1221-1228), retry loop (line 1152) | | **LLM Selection** | [`agent_go/pkg/orchestrator/agents/workflow/step_based_workflow/controller_agent_factory.go`](../../agent_go/pkg/orchestrator/agents/workflow/step_based_workflow/controller_agent_factory.go) | `selectExecutionLLM()` - LLM selection logic (lines 228-270) | | **Validation Check** | [`agent_go/pkg/orchestrator/agents/workflow/step_based_workflow/controller_execution.go`](../../agent_go/pkg/orchestrator/agents/workflow/step_based_workflow/controller_execution.go) | `isValidationFailure()` function (lines 46-54) | ### 🔄 Flow Sequence ```mermaid graph TD A[Execution Start] --> B{Check Conditions} B -->|step execution_llm set| C[Step Override] B -->|sub_agent_llm available| D[Sub-agent Override] B -->|tier resolver active| E[Tiered Resolution] B -->|otherwise| F[Workflow Execution LLM] C --> G[Run Step] D --> G E --> G F --> G G -->|retry if needed| B ``` ### Attempt Sequence **File**: [`controller_agent_factory.go`](../../agent_go/pkg/orchestrator/agents/workflow/step_based_workflow/controller_agent_factory.go) **Priority Order** (checked in this sequence): 1. **Step execution LLM** - Used when `agent_configs.execution_llm` is set for the step. 2. **Sub-agent override** - Used when execution is running inside a sub-agent context that supplies `sub_agent_llm`. 3. **Tiered execution resolution** - Used when tiered mode is active and no step override is present. 4. **Original LLM chain** (preset/workflow execution LLM) - Used as the remaining fallback. ### ⚙️ Failure Criteria #### For retry purposes **Only `ExecutionStatus == "FAILED"` counts as failure:** | Status | Action | Triggers Retry? | |--------|--------|-----------------| | `COMPLETED` | Success | ❌ No retry | | `PARTIAL` | Success | ❌ No retry | | `INCOMPLETE` | Success | ❌ No retry | | `FAILED` | Failure | ✅ Triggers next attempt | #### Validation Status Handling **File**: [`controller_execution.go:46-54`](../../agent_go/pkg/orchestrator/agents/workflow/step_based_workflow/controller_execution.go#L46) **Retry Decision**: Uses `IsSuccessCriteriaMet` from validation response - If `IsSuccessCriteriaMet == true`: Stop retry, step passes - If `IsSuccessCriteriaMet == false`: Continue retry (regardless of status) **Retry handling**: Uses `ExecutionStatus == "FAILED"` via `isValidationFailure()` - Only `ExecutionStatus == "FAILED"` triggers `isRetryAfterValidationFailure` - `PARTIAL`/`INCOMPLETE`/`COMPLETED` with unmet criteria still retry, but they do not change model selection **Special Cases**: - **Loop Iterations**: New loop iterations after failure (`loopIterationCount > 1`) trigger `isRetryAfterValidationFailure`. ### 🔄 Implementation Details #### Key Logic **File:** [`controller_execution.go`](../../agent_go/pkg/orchestrator/agents/workflow/step_based_workflow/controller_execution.go) ```go // Validation failure check isRetryAfterValidationFailure := isValidationFailure(previousValidationResponse) && (retryAttempt > 1 || (hasLoop(step) && loopIterationCount > 1)) ``` **File:** [`controller_agent_factory.go`](../../agent_go/pkg/orchestrator/agents/workflow/step_based_workflow/controller_agent_factory.go) ```go if stepConfig.ExecutionLLM != nil { // Use explicit step execution LLM } else if subAgentLLMFromContext != nil { // Use sub-agent override when allowed } else if tierResolver != nil { // Resolve execution tier from context and learning maturity } else { // Use workflow/preset execution LLM fallback } ``` #### Conditions **File**: [`controller_agent_factory.go:131-236`](../../agent_go/pkg/orchestrator/agents/workflow/step_based_workflow/controller_agent_factory.go#L131) | Condition | Purpose | Effect | Notes | |-----------|---------|--------|-------| | Step `execution_llm` set | Explicit per-step model choice | Overrides all runtime execution model selection | Applies in both manual and tiered modes | | `sub_agent_llm` present in context | Use parent-selected model for nested execution | Overrides tiered/preset selection when step config is absent | Only applies to sub-agent executions | | Tier resolver configured | Use maturity-based execution tier | Selects tiered execution model when no step override is present | Active in tiered mode | | No step override and no tier resolver result | Fall back to workflow execution LLM | Uses preset/workflow execution config | Final execution fallback | ### 🛠️ Common Issues & Solutions | Issue | Cause | Solution | |-------|-------|----------| | Step model not used | `execution_llm` not set on the step | Set `agent_configs.execution_llm` for that step | | Tiered model used unexpectedly | Tiered mode is active and the step has no `execution_llm` | Set a step execution LLM or change workflow allocation mode | | Sub-agent model used unexpectedly | Execution is happening inside a sub-agent context | Check the calling sub-agent configuration and context | | Always uses preset/workflow model | No step override and no tier resolver match/override | Verify workflow LLM allocation and step config | ### 🔍 For LLMs: Quick Reference **Constraints:** - ✅ **Allowed**: Set `execution_llm` on a step to force that execution model. - ✅ **Allowed**: Let tiered mode select execution models when no step override is present. - ✅ **Allowed**: Fall back to the workflow execution LLM when no higher-priority override exists. **Failure Detection:** - Only `ExecutionStatus == "FAILED"` triggers `isRetryAfterValidationFailure` - Decision step false result also triggers `isRetryAfterValidationFailure` - New loop iteration after failure (`loopIterationCount > 1`) triggers `isRetryAfterValidationFailure` - `PARTIAL`/`INCOMPLETE`/`COMPLETED` with unmet criteria continue retry but do not change model selection **Priority Order:** 1. Step `execution_llm` 2. `sub_agent_llm` 3. Tiered execution resolution 4. Workflow execution LLM fallback **Example Flows:** **Step override present:** ``` Attempt 1: Step execution LLM → FAILED → Continue Attempt 2: Step execution LLM → SUCCESS → Complete ``` **Tiered execution:** ``` Attempt 1: Tier-selected model → FAILED → Continue Attempt 2: Tier-selected model → SUCCESS → Complete ``` **Decision Step False:** ``` Decision Step: FALSE → isRetryAfterValidationFailure = true Attempt 1: Selected execution model → SUCCESS → Complete ``` **Loop Iteration After Failure:** ``` Loop Iteration 1: Selected execution model → FAILED Loop Iteration 2: Selected execution model → SUCCESS ``` --- ## 📖 Related Documentation - [Workflow Docs](../workflow/README.md) - Overall execution system - [Controller Execution](../../agent_go/pkg/orchestrator/agents/workflow/step_based_workflow/controller_execution.go) - Retry logic implementation - [Model Metadata](https://github.com/manishiitg/multi-llm-provider-go/blob/main/llmtypes/model_metadata.go) - Pricing, context, capabilities - [LLM Store](../../frontend/src/stores/useLLMStore.ts) - State management ## 🌉 MCP Bridge Layer & Exposed APIs Canonical HTML: https://agentworkshq.com/docs/core/mcp_bridge_layer/ Raw Markdown: https://agentworkshq.com/docs-content/core/mcp_bridge_layer.md # 🌉 MCP Bridge Layer & Exposed APIs The **MCP Bridge Layer** is a critical architectural component of Runloop. It acts as a universal proxy and translation layer, allowing external local CLI agents (like **Claude Code** and **Gemini CLI**) to seamlessly access the orchestrator's entire suite of loaded tools, virtual tools, and workspace capabilities. ## Architecture Overview Instead of requiring every agent to manage its own MCP server connections, the orchestrator centralizes tool discovery, session management, and routing. External agents use a lightweight proxy binary called `mcpbridge` to communicate with the orchestrator's REST API. 1. **Local CLI Agent** (e.g., `claude-code`, `gemini`) spawns `mcpbridge`. 2. **mcpbridge** translates the stdio-based MCP protocol into HTTP REST calls. 3. **Orchestrator** processes the HTTP request, executes the tool (routing to Docker, external APIs, or the local Workspace), and returns the result. --- ## 🔌 Exposed APIs The orchestrator exposes several API layers to facilitate this bridge and manage the workspace. ### 1. Tool Execution & Discovery (Bridge API) These endpoints are consumed by `mcpbridge` and the web UI to interact with tools. * **`POST /api/mcp/execute`** * The central routing endpoint. Accepts a JSON payload containing the `server_name`, `tool_name`, and `arguments`. * Automatically routes the request to the correct underlying MCP server, virtual tool, or workspace capability. * **`GET /api/tools`** * Returns a flattened, formatted schema of all available tools across all connected MCP servers and virtual tools. * **`GET /api/mcp-config` & `POST /api/mcp-config`** * Retrieves and updates the active MCP server configuration (`mcp_servers.json`). * **`POST /api/mcp-config/discover`** * Forces a manual discovery and reconnection to all configured MCP servers. ### 2. Workspace API (Port 8080) The `workspace` container runs a dedicated Planner API that the orchestrator interfaces with for local file and system management. * **Document Management:** * `GET /api/documents` / `POST /api/documents` * `GET/PUT/DELETE /api/documents/*filepath` * `GET /api/glob` (File discovery via glob patterns) * **Search & Semantic Intelligence:** * `GET /api/search` (Lexical grep search) * `POST /api/search/process-file` * **System & Execution:** * `POST /api/execute` (Secure shell command execution within the workspace) * `POST /api/upload` (File uploads) --- ## 🌎 Universal Stdio Gateway The `mcpbridge` binary isn't just for Claude or Gemini—it serves as a **Universal Stdio Gateway**. Any external LLM agent, framework (LangChain, AutoGPT, CrewAI), or custom script that supports the Model Context Protocol (MCP) via `stdio` can use `mcpbridge` to access your entire tool ecosystem through a single connection. ### Why use the Centralized Bridge? * **Single Connection, 100+ Tools**: Connect to one "server" (`mcpbridge`) and immediately gain access to every tool configured in your orchestrator (Slack, Workspace, Browser, etc.). * **Centralized Auth**: Manage API keys and OAuth tokens in one place (the orchestrator) rather than in every individual agent script. * **Session Isolation**: Use the `MCP_SESSION_ID` environment variable to isolate tool execution and folder guards between different external agents. * **Monitoring**: Every tool call made via the bridge is logged and visible in the orchestrator's central dashboard. ### Configuration for External Frameworks To connect an external agent, simply point it to the `mcpbridge` binary: ```bash # Example: Launching a custom MCP-compatible agent my-agent-cli --mcp-command "mcpbridge" --mcp-env "MCP_API_URL=http://localhost:8080/api" ``` ## Secrets Canonical HTML: https://agentworkshq.com/docs/core/secrets/ Raw Markdown: https://agentworkshq.com/docs-content/core/secrets.md # Secrets Secrets allow sensitive credentials (API keys, database passwords, tokens) to be securely injected into agent queries, workflow steps, and delegated sub-agents without exposing them in chat history or logs. There are three types of secrets: | Type | Defined by | Stored in | Scope | |------|-----------|-----------|-------| | **Workflow Secrets** | End user via workflow edit UI or builder tools | Server-side per-user encrypted store | One workflow, opt-in selection | | **User Secrets** | End user via frontend UI | Browser localStorage plus server-side per-user encrypted store when synced | Reusable by the user, opt-in selection | | **Global Secrets** | Admin via environment variables | Server memory (plaintext from env) | All queries, always included | ## User Secrets ### How It Works 1. User creates a secret in the **Secrets Manager** modal (name + value). 2. The value is encrypted server-side using AES-256-GCM (derived from `AUTH_SECRET`) and stored in the browser's localStorage. 3. When sending a query, the user selects which secrets to include. 4. Selected secrets are decrypted server-side and injected into the agent's prompt at runtime. ## Workflow Secrets Workflow secrets are encrypted with the same AES-256-GCM mechanism as user secrets, but are stored under a per-user/per-workflow key in workspace docs: ```text _users//workflow_secrets/.json ``` Only names are stored in `workflow.json` under `capabilities.selected_secrets`. Runtime resolves each selected name from the workflow store first, then the reusable user store, then global secrets. ### Encryption - **Algorithm**: AES-256-GCM - **Key derivation**: HMAC-SHA256 of `AUTH_SECRET` with context `"secrets-encryption-key"` - **Per-user isolation**: User ID is used as Additional Authenticated Data (AAD), preventing cross-user decryption. ### API Endpoints | Method | Path | Description | |--------|------|-------------| | POST | `/api/secrets/encrypt` | Encrypt a plaintext value | | POST | `/api/secrets/decrypt` | Decrypt an encrypted value | ## Global Secrets Global secrets are admin-defined credentials loaded from environment variables at server startup. They are automatically merged into every query (chat, workflow, delegation) without user interaction. ### Configuration Set environment variables with the `GLOBAL_SECRET_` prefix: ```bash GLOBAL_SECRET_OPENAI_KEY=sk-xxx GLOBAL_SECRET_DB_PASSWORD=pass123 GLOBAL_SECRET_GITHUB_TOKEN=ghp_abc123 ``` The secret name is derived from the suffix after `GLOBAL_SECRET_`: - `GLOBAL_SECRET_OPENAI_KEY` becomes secret name `OPENAI_KEY` - `GLOBAL_SECRET_DB_PASSWORD` becomes secret name `DB_PASSWORD` Values can be plain strings or JSON: ```bash GLOBAL_SECRET_CLICKHOUSE_CONFIG={"host":"10.0.0.1","port":"8123","user":"reader","password":"secret"} ``` ### Kubernetes Deployment For K8s deployments, add global secrets to the `deploy/k8s/.env` file (gitignored), then load them into your K8s Secret resource (`prod-mcpagent-secret`). The agent deployment already mounts this secret via `envFrom`. ### API Endpoint | Method | Path | Description | |--------|------|-------------| | GET | `/api/secrets/global` | Returns global secret names (no values) | Response: ```json [{"name": "OPENAI_KEY"}, {"name": "DB_PASSWORD"}] ``` ### Merge Behavior When a query is processed, global secrets are merged with user-selected secrets: 1. Global secrets are prepended to the list. 2. If a user secret has the same name as a global secret, the **user secret wins** (user override). 3. The merged list is injected into chat queries, workflow orchestrators, and delegated sub-agents. ### Frontend Display Global secrets appear in both the **Secrets Manager** modal and the **Secret Selection** panel: - Shown as read-only entries with a "Global" badge - Values are masked (`••••••••`) — not decryptable from the frontend - No checkbox — always included automatically - Displayed above user secrets with a visual separator ## Injection Format Secrets are injected into agent prompts in this format: ``` 🔐 Secrets: ### SECRET_NAME \``` secret_value \``` ``` For workflow mode, secrets are passed to the orchestrator which injects them into each step's system prompt. For delegation, the parent's merged secrets are inherited by sub-agents. ## Skills System Canonical HTML: https://agentworkshq.com/docs/core/skills/ Raw Markdown: https://agentworkshq.com/docs-content/core/skills.md # Skills System Skills are reusable instruction sets that guide workflow agents on how to handle specific tasks. They provide domain expertise and methodology that agents can apply during step execution. ## Overview - **Storage**: Skills are stored in `workspace-docs/skills/`. - **Standard Skills**: Imported skills are stored in `skills//`. - **Custom Skills**: Skills created via Skill Builder are stored in `skills/custom//`. - **Main File**: Each skill must have a `SKILL.md` file with YAML frontmatter. - **Read-Only**: Skills are read-only during workflow execution (but writable in Skill Builder mode). - **Selection**: Skills can be selected at preset level or overridden per-step. ## Skill Builder Mode The Skill Builder is a specialized agent mode designed to help you create, test, and refine skills. - **Access**: Select "Skill Builder" from the mode switcher or use `Ctrl+5`. - **Theme**: Identified by the Emerald (Green) theme. - **Capabilities**: - Automatically creates skills in the `skills/custom/` directory. - Can read all existing skills for reference. - Restricted to writing only within `skills/custom/`. - Optimized for creating API wrappers and automation scripts (Python/Bash). ## Skill File Format A valid skill folder must contain `SKILL.md`: ```markdown --- name: code-review description: Performs a comprehensive code review argument-hint: allowed-tools: ["execute_shell_command", "diff_patch_workspace_file"] model: openrouter/anthropic/claude-sonnet-4 --- Review the code at `$ARGUMENTS` focusing on: 1. Code quality 2. Potential bugs 3. Security issues ``` ### Frontmatter Fields | Field | Required | Description | |-------|----------|-------------| | `name` | Yes | Display name of the skill | | `description` | Yes | Brief description of what the skill does | | `argument-hint` | No | Hint for arguments the skill accepts | | `allowed-tools` | No | List of tools the skill is allowed to use. For current workflow agents, prefer exposed tools such as `execute_shell_command` and `diff_patch_workspace_file`; legacy workspace-basic names like `read_workspace_file`, `update_workspace_file`, and `delete_workspace_file` are internal/API wrappers unless explicitly exposed by the session. | | `model` | No | Preferred LLM model for this skill | | `disable-model-invocation` | No | When true, the model is not offered this skill for automatic invocation | | `user-invocable` | No | Whether the skill appears as user-invocable (e.g. as a slash command); auto-generated learnings skills set this to `false` | | `effort` | No | Effort hint for the skill's execution | | `context` | No | Context hint for skill matching | | `agent` | No | Agent hint for skill routing | ## Configuration Hierarchy Skills follow a priority hierarchy: ``` PresetQuery.selected_skills (workflow-wide default) └── Step.AgentConfigs.enabled_skills (per-step override) ``` 1. **Preset Level**: Default skills for all steps in the workflow 2. **Step Level**: Override in `step_config.json` for specific steps ### Example step_config.json ```json { "steps": [ { "id": "research-step", "agent_configs": { "selected_servers": ["playwright"], "enabled_skills": ["lead-research-assistant"] } }, { "id": "mcp-building-step", "agent_configs": { "selected_servers": ["github"], "enabled_skills": ["mcp-builder"] } } ] } ``` ## Backend Implementation ### Package Structure ``` agent_go/pkg/skills/ ├── types.go # Skill, SkillFrontmatter structs ├── parser.go # Parse YAML frontmatter + markdown ├── validator.go # Validate skill folder against spec ├── discovery.go # Discover skills from workspace-docs/skills/ (incl. custom/) ├── github.go # Download skill folders from GitHub URLs ├── workspace_api.go # Workspace file operations ├── runtime_loader.go # LoadAttachable / LoadGlobalSkill — build attachable skills for agents ├── builtin_browser_skills.go # Builtin agent-browser / playwright skills (served from code, never on disk) ├── cli.go # skills CLI integration (npx skills), lock-file update detection └── zip.go # Zip import/export of skill folders ``` ### Key Types ```go type SkillFrontmatter struct { Name string `yaml:"name,omitempty"` Description string `yaml:"description"` ArgumentHint string `yaml:"argument-hint,omitempty"` AllowedTools []string `yaml:"allowed-tools,omitempty"` Model string `yaml:"model,omitempty"` DisableModelInvocation bool `yaml:"disable-model-invocation,omitempty"` UserInvocable *bool `yaml:"user-invocable,omitempty"` Effort string `yaml:"effort,omitempty"` Context string `yaml:"context,omitempty"` Agent string `yaml:"agent,omitempty"` } type Skill struct { Frontmatter SkillFrontmatter `json:"frontmatter"` Content string `json:"content"` // Markdown after frontmatter FolderName string `json:"folder_name"` // Skill folder name FilePath string `json:"file_path"` // Relative path in workspace SourceURL string `json:"source_url,omitempty"` // Source URL (from lock file) LockInfo *CLILockEntry `json:"lock_info,omitempty"` // Version tracking from skills-lock.json } ``` ### API Endpoints | Method | Endpoint | Description | |--------|----------|-------------| | GET | `/api/skills` | List all skills in workspace-docs/skills/ | | GET | `/api/skills/{name}` | Get skill details and content | | POST | `/api/skills/import` | Import skill from GitHub folder URL | | POST | `/api/skills/validate` | Validate a GitHub URL before import | | DELETE | `/api/skills/{name}` | Delete a skill folder | | PUT | `/api/skills/{name}` | Update skill content | ### Workflow Integration Skills are integrated into workflow execution in `skills_integration.go`: ```go // Get effective skills for a step (step override > preset default) func GetEffectiveSkills(stepConfig *AgentConfigs, orchestrator *BaseOrchestrator) []string // Build folder guard paths (skills are read-only) func BuildSkillFolderGuardPaths(selectedSkills []string) (readPaths, writePaths []string) ``` ### How Skills Reach the Agent There is no hand-assembled "Active Skills" prompt section any more. Skill surfacing lives in the transport layer: 1. Builders call `skills.LoadAttachable(workspaceAPIURL, selectedSkills)` (`pkg/skills/runtime_loader.go`), which parses each SKILL.md into an attachable skill — full markdown body plus `SupportingFiles` (everything under the skill folder except SKILL.md, e.g. `references/`, `scripts/`). 2. Each skill is registered with `agent.AttachSkill(skill)`. The mcpagent library injects a progressive-disclosure skill listing into the outgoing system prompt at `ensureSystemPrompt()` time; CLI transports additionally project the skill folder to disk via the SkillProjector contract. 3. **Builtin skills**: `agent-browser` and `playwright` are served from code (`builtin_browser_skills.go`), not from the skills/ folder. The loader checks builtins first, so a disk folder with the same name would be silently shadowed — never create one. 4. **Global learnings pointer**: `LoadGlobalSkill()` attaches a tiny "workflow-learnings" pointer skill that directs the agent to read `learnings/_global/` from the workflow folder on demand, instead of copying the (large, growing) learnings bundle into every session. ### Folder Guard Skills folders are added to the read-only paths in the folder guard: ```go // In controller_agent_factory.go effectiveSkills := GetEffectiveSkills(stepConfig, orchestrator) if len(effectiveSkills) > 0 { skillReadPaths, _ := BuildSkillFolderGuardPaths(effectiveSkills) readPaths = append(readPaths, skillReadPaths...) } ``` This ensures agents can read skill files but cannot modify them. ## Frontend Implementation ### Components ``` frontend/src/components/skills/ ├── SkillsManager.tsx # Main panel (sidebar) ├── SkillsList.tsx # List of installed skills ├── SkillCard.tsx # Individual skill display ├── SkillImportDialog.tsx # Dialog to paste GitHub URL ├── SkillEditor.tsx # View/edit skill content └── SkillSelectionSection.tsx # Multi-select for preset editor ``` ### API Client ```typescript // frontend/src/api/skills.ts export const skillsApi = { listSkills(): Promise<{ skills: Skill[] }> getSkill(folderName: string): Promise importSkill(githubUrl: string): Promise validateSkill(githubUrl: string): Promise deleteSkill(folderName: string): Promise updateSkill(folderName: string, content: string): Promise } ``` ### Preset Integration Skills are stored in presets via `selected_skills` field: ```typescript interface CustomPreset { // ... other fields selectedSkills?: string[]; // Array of skill folder names } ``` The `SkillSelectionSection` component allows selecting skills when creating/editing workflow presets. ## GitHub Import Skills can be imported from GitHub folder URLs: 1. User provides URL like `https://github.com/user/repo/tree/main/skills/my-skill` 2. Backend parses URL to extract owner, repo, branch, path 3. Fetches folder contents via GitHub API 4. Validates: checks for SKILL.md, parses frontmatter, validates required fields 5. Downloads files to `workspace-docs/skills/{skill-name}/` 6. Returns success/error to frontend ## Storage Structure ``` workspace-docs/ ├── skills/ │ ├── code-review/ # Standard/Imported Skill │ │ └── SKILL.md │ ├── custom/ # User-created Skills │ │ └── my-new-skill/ │ │ └── SKILL.md │ └── lead-research-assistant/ │ ├── SKILL.md │ └── templates/ │ └── research-template.md └── ... other workspace files ``` ## Usage Flow 1. **Import Skills**: Use Skills Manager to import from GitHub or create manually 2. **Create Preset**: Select skills in the preset editor for workflow-wide defaults 3. **Configure Steps**: Optionally override skills per-step in step_config.json 4. **Execution**: Agent reads skill instructions and applies methodology to step ## Multiagent Docs Canonical HTML: https://agentworkshq.com/docs/multiagent/ Raw Markdown: https://agentworkshq.com/docs-content/multiagent/README.md # Multiagent Docs These docs cover delegation, multi-agent chat, and coordination between manager and worker agents. ## Includes - Sub-agent spawning (direct delegation, no planning phase) - Multi-agent chat session behavior - Memory tools and cross-session recall - Operator controls that primarily support delegation workflows ## Files - `agent_memory_system.md` - `multi_tab_chat_architecture.md` - `slash_commands.md` - `sub_agent_delegation.md` ## Agent Memory System Canonical HTML: https://agentworkshq.com/docs/multiagent/agent_memory_system/ Raw Markdown: https://agentworkshq.com/docs-content/multiagent/agent_memory_system.md # Agent Memory System Persistent memory for the multi-agent chat mode. Memories survive across sessions and are stored as markdown files in the workspace. ## Overview The memory system provides three virtual tools — `save_memory`, `recall_memory`, and `enrich_memory` — available in **Multi-Agent Chat mode**. Each tool spawns an asynchronous background sub-agent that performs operations using shell commands. The manager agent is notified when the background agent completes via the same synthetic-turn mechanism used by `delegate()`. ## Tools ### save_memory | Parameter | Required | Description | |-----------|----------|-------------| | `content` | Yes | The detailed information to save (including context, alternatives, and outcomes). | | `context` | No | Additional context about why this is being saved. | Returns immediately with an `agent_id`. A background agent (running with `medium` reasoning): 1. Creates the current day folder (e.g., `memories/YYYY-MM-DD/`) 2. Checks existing files for duplicates 3. Appends (or creates) a category file (like `decisions.md` or `general.md`) with a timestamped heading 4. Identifies up to 3 key entities in the text and updates their respective entity files in the `entities/` folder. ### recall_memory | Parameter | Required | Description | |-----------|----------|-------------| | `query` | Yes | Topic, keyword, or question to search for. | Returns immediately with an `agent_id`. A background agent (running with `low` reasoning): 1. **Priority 0 (Index):** If the query is broad or seeks orientation (e.g., "index"), it reads `index.md` first. 2. **Priority 1 (Entity Fast Path):** Checks the `entities.md` registry and directly reads matching entity files. 3. **Priority 2 (Date Search):** Greps across all chronological `YYYY-MM-DD/` folders. 4. Synthesizes a combined summary of all findings. ### enrich_memory | Parameter | Required | Description | |-----------|----------|-------------| | `focus` | No | Optional topic to focus consolidation on (Phase 1–4). Chat-history extraction (Phase 0) still runs for all sessions. | | `delete_older_than_days` | No | Delete chat sessions older than this many days after extraction (default `7`). Set to `0` to disable deletion. | Returns immediately with an `agent_id`. A background agent (running with `medium` reasoning): 1. **Phase 0 — Distil chat history:** Reads every session in `{chat_history}/`, extracts lasting-value insights (preferences, decisions, what worked/failed, project context, recurring patterns, key facts) into today's date folder and entity files. After extraction, deletes session folders older than `delete_older_than_days` via `find -mtime +N -exec rm -rf`. 2. **Phase 1–3 — Consolidate:** Reads all date and entity files. Identifies redundant, superseded, duplicate, or overly verbose entries. Consolidates and deduplicates, rewriting the markdown files cleanly and removing empty folders / stale entities. 3. **Phase 4 — Regenerate Index:** Builds a fresh `index.md` file containing key decisions, project state, active entities, and orientation guidance for future sessions. **Rule:** the agent does NOT save facts that can be looked up live from workflows, MCP servers, or APIs (e.g. current PR status, channel lists, live metrics). Memory is for things that cannot be rediscovered from live data — decisions, preferences, learned patterns. ## Storage Structure The system uses parallel structures for different access patterns, defaulting to the workspace-root `memories/` folder (this path can be overridden per session via the `MemoryFolderKey` context key — for example, a per-project memory store): ``` memories/ index.md ← High-level snapshot (regenerated by enrich_memory) prompt.md ← User-editable custom instructions (optional) entities.md ← Entity registry (list of known entity names) entities/ ← Per-entity knowledge files (fast lookup) auth-service.md ← Everything known about "auth-service" postgresql.md ← Everything known about "postgresql" 2026-04-11/ ← Daily date folders (chronological log) general.md decisions.md preferences.md ``` ### 1. index.md (The Orientation File) Generated by `enrich_memory`, this is the primary orientation file read at the start of a session or before making architectural decisions. It contains: - **Key Decisions:** Settled decisions that shouldn't be re-litigated. - **Project State:** Current progress and direction. - **Active Entities:** A list of known entities. - **What to Check:** Topics that warrant a deeper `recall_memory`. ### 2. Entity Files (entities/) Entity files group all knowledge about a specific named thing (project, system, technology, person, feature). They provide O(1) lookup speed. They are updated by `save_memory` when it extracts relevant entities, and cleaned up by `enrich_memory`. ### 3. Chronological Log (YYYY-MM-DD/) A full historical log of all memories stored by date. Categories include `decisions.md`, `general.md`, `preferences.md`, etc. ## Custom Instructions (prompt.md) Users can customize memory agent behavior by creating `prompt.md` in the memory folder. This file is loaded dynamically via the workspace API and prepended to the background agent's system prompt. Example `prompt.md`: ```markdown - Always save memories in bullet-point format - Categorize by project name (e.g., auth-service.md, data-pipeline.md) - Never save code snippets — only decisions and rationale - Use Spanish for all memory entries ``` ## Architecture ### Execution Flow The virtual tools map to handler functions in Go that spawn async sub-agents: ``` Manager Agent ↓ calls save_memory() / recall_memory() / enrich_memory() ↓ Handler Function (e.g., handleSaveMemory) ├── Reads {memoryFolder}/prompt.md via workspace API (if exists) ├── Builds highly specific instruction string for the sub-agent ├── Sets reasoning level (medium for save/compress, low for recall) ├── Calls BackgroundDelegateFunc(ctx, AgentName, instruction) └── Returns immediately with { agent_id, status: "running" } ↓ Background Sub-Agent (async) ├── Uses execute_shell_command for all file operations (mkdir, ls, cat, grep, rm) ├── Executes memory logic (writing files, grepping, consolidating) └── Manager is notified on completion ``` ### Context Injection The memory tool executors receive these context values: | Context Key | Value | Purpose | |-------------|-------|---------| | `MemoryFolderKey` | `memory_folder` string | Overrides the default `Chats/memories` path | | `WorkspaceClientKey` | `workspace.Client` | Reading `prompt.md` via API | | `BackgroundDelegateKey` | `BackgroundDelegateFunc` | Spawning background agents | ## Files | File | Purpose | |------|---------| | `agent_go/cmd/server/virtual-tools/memory_tools.go` | Tool definitions, async executors, agent instructions (`GetMemoryInstructions`) | | `agent_go/cmd/server/server.go` | Tool registration and context injection for multi-agent chat | ## Org Pulse — the Chief of Staff's daily heartbeat Canonical HTML: https://agentworkshq.com/docs/multiagent/org_pulse_design/ Raw Markdown: https://agentworkshq.com/docs-content/multiagent/org_pulse_design.md # Org Pulse — the Chief of Staff's daily heartbeat Status: **Design** (2026-06-21). Not implemented. ## The model this sits in The **Chief of Staff (CoS)** is the multi-agent chat — a single agent identity that runs the org. It stands on **two primitives** (2026-06-22 — collapsed from four): | Primitive | What it is | |---|---| | **Memory** | what the system *knows* — durable, entity-based (`entities/*.md`), auto-enriched. Includes procedural "how we do X" knowledge applied inline. | | **Workflow** | what the system reusably *does* — any repeatable task, one step or twenty (own plan / eval / Pulse / backup). | Everything else dissolves into these two: - **Ad-hoc task** — not a stored thing; it's just chat. If it recurs it *becomes a workflow*. Transient activity, not a primitive. - **Skill** — deleted as a separate tier. A *runnable* reusable procedure is just a small **workflow**; *how-to knowledge* the agent applies inline is just **memory**. - **Employee** — already dissolved into memory (entities). - **Chief of Staff** — the operator over the two axes, not a stored thing. ### The promotion ladder (now one rung) Collapsing "skill" deletes the painful skill↔workflow boundary. The ladder is just: **recurring ad-hoc task → workflow.** A workflow can be one step, so promoting a small reusable task isn't heavy. - `create_workflow` — **exists** (server.go:4419, privileged; confirmed callable from the CoS chat). The promotion rung is already there. - ~~`save_skill`~~ — **dropped (2026-06-22)**. No skill tier, so nothing to build here. - A unified **ledger** of what the CoS did (ad-hoc + scheduled) — still useful as the substrate for noticing recurrence; not yet built. **Caveat:** "reusable task = workflow" only holds while `create_workflow` stays lightweight (a minimal manifest + one-step plan). Keep that true. ## Org Pulse — what it is A **daily, opt-in pass on the CoS chat** — the chat-level parallel to the per-workflow Pulse toggle (`post_run_monitor`). When **on**, once a day the CoS: 1. **Judges the endgame** — reads each workflow's Pulse verdicts (the Bug/Goal pills + goal card in `builder/improve.html` and the `card.health.html` dashboard card, written by the per-workflow Pulse) and rolls the Goal verdicts into an org-level "are we achieving our goals" view. Cheap — the per-workflow Pulse already did the judging. 2. **Harvests** — reads each workflow's `reports/`, `knowledgebase/`, and `learnings/_global/SKILL.md`, **plus the stored conversation files**, and curates the worth-keeping bits into its own shared memory (entities/topics). 3. **Suggests** — surfaces decisions for the user, including **promotion proposals** ("you've done X 3× — make it a workflow?"). The ladder is driven by these suggestions. The per-workflow Pulse is the **foundation**: Org Pulse aggregates its verdicts and harvests its learnings. Doing per-workflow Pulse first was the right order. ## Keep it agentic, NOT an import This is the load-bearing constraint (see the agentic-not-deterministic principle). - **Wrong (import):** a Go job that parses Pulse verdicts + learnings on a schedule and copies fields into a CoS KB table. Fixed schema, 1:1 mirror, rots. - **Right (harvest):** Org Pulse is a **CoS reasoning session driven by a reference doc**. Go supplies only **access** (read tools it already has), **cadence** (the daily schedule), and **the contract** (the doc). The agent **judges** relevance, **synthesizes across** workflows, and **curates** into its own knowledge (merge/update, not append). - Precedent to copy: the workflows' own agentic curation — the KB-update agent, `consolidate_knowledgebase`, `organize_global_learnings`. Org Pulse is the same pattern, one level up. - **Memory store choice:** free-form agent-curated memory (`entities/`/topics), NOT a structured DB table — a rigid schema drags it back toward import. A structured store only earns its place later if the org knowledge needs programmatic querying, and even then the agent writes it (like the workflows' `graph.json`). The rule: every time you're tempted to add a Go step that parses a workflow file and writes a CoS file, that's an import — make it a line in the reference doc instead. ## Why the build is small (most plumbing exists) - **Schedule mechanism:** the CoS already runs builtin scheduled passes (`builtin-auto-enrich-memory`, every 3h — `builtin_schedules.go`). Org Pulse is the same shape: a builtin **daily** schedule, gated by a toggle. - **Toggle pattern:** workflow Pulse is the `post_run_monitor` flag flipping a scheduled pass on. Org Pulse is a flag in the CoS config (`multiagent-config.json`), surfaced as an on/off in the chat header. - **Conversations are files:** chat history is persisted (`chat_history_persistence`), so the pass can read past conversations as evidence. - **Access:** the CoS already has read-only filesystem access to every `Workflow//` via `execute_shell_command`. ## Build pieces 1. **Toggle + cadence** — an Org Pulse on/off in the CoS chat header. It's a schedule, so it carries a `cron_expression`: **default once a day, user-editable** (change the time or frequency like any other schedule). Stored alongside the other multi-agent schedules. *(Frontend chat-header toggle still pending; enable/disable already works via the Scheduled Tasks popup since the builtin is registered.)* 2. **Daily builtin schedule** — ✅ Built (2026-06-21). `builtin-org-pulse` added to `DefaultBuiltinSchedules()` (`builtin_schedules.go`): **`Enabled:false` (opt-in)**, cron `0 8 * * *` (daily, editable), mode multi-agent, query loads `get_reference_doc(kind="org-pulse")`. Turning it on = a same-ID user override with `enabled:true` + chosen cron (via `MergeBuiltinSchedules`). **No Go pre-fire check** — Org Pulse self-gates agentically (wakes daily, cheap "anything new?" check, exits if not), unlike enrich-memory's Go gate. Idle-day behavior: a clean no-op (writes nothing, stops). Build + tests pass. 3. **The Org Pulse reference doc** — ✅ Drafted (2026-06-21): `guidance/templates/system/org-pulse.md`, registered as `get_reference_doc(kind="org-pulse")` (multi-agent mode) in `guidance.go`. 5-step agentic contract: gather → judge endgame → harvest (curate/merge, never import) → spot promotions → record everything in the single **`pulse/org-pulse.html`** log (HTML-only, no JSON — decided 2026-06-22), notify only when decision-worthy. Build + guidance tests pass. 4. **Extend the CoS access recipe** — ✅ Done (2026-06-22). `employee-management.md` sweep now reads each workflow's Pulse health (improve.html pills / `card.health.html`), `knowledgebase/`, and `learnings/_global/SKILL.md`, not just runs/db/reports. 5. **Org Pulse view** — ✅ Done (2026-06-22). HTML-only: an **"Org Pulse" button** in the CoS chat header (`ChatTabs.tsx`) opens `pulse/org-pulse.html` in the existing right-side file viewer (renders via `HtmlRenderer`), the multi-agent parallel of a workflow's Pulse tab. No JSON inbox, no accept/dismiss — suggestions are entries in the HTML. 6. *(Ladder, later — optional)* a unified **ledger** of CoS activity as the recurrence substrate. `save_skill` is **dropped** — promotion goes straight to `create_workflow` (which already works from chat), so there's no skill rung to build. ## Decided - **Cadence (2026-06-21):** default **once a day, user-configurable** — it's a normal multi-agent schedule with an editable `cron_expression`, not a fixed time. - **Notify (drafted into org-pulse.md):** silent on a steady day; one push only on a decision-worthy change (mirrors workflow Pulse's transition discipline). - **Surface (2026-06-22):** **HTML-only**, shown on the right like a workflow's Pulse — `pulse/org-pulse.html` opened via the CoS chat-header "Org Pulse" button. No `suggestions.json`, no accept/dismiss inbox; suggestions are cards inside the HTML. ## Chat-header control (✅ Done 2026-06-22) `OrgPulseControl.tsx` in the CoS chat header (`ChatTabs.tsx`) — mirrors the workflow Pulse toggle. An **"Org Pulse · ON/OFF"** button opens a popup with: an explanation, an **enable/disable switch** (flips the `builtin-org-pulse` schedule via `schedulerApi.enableJob/disableJob`, with next/last-run status), and **"Open today's log"** (opens `pulse/org-pulse.html` on the right). **Empty state** handled here: before the first run it shows "No Org Pulse log yet" + a **Run now** button (`triggerJob`) to generate it on demand — so we never dump the user into an empty file viewer. ## Open questions - *(none — surface, cadence, notify, the ladder, the toggle, and the empty state are all settled/built.)* ## Workflow Docs Canonical HTML: https://agentworkshq.com/docs/workflow/ Raw Markdown: https://agentworkshq.com/docs-content/workflow/README.md # Workflow Docs These docs cover workflow design, execution, and workflow-scoped runtime behavior. ## Includes - Canvas and workflow UX - Running workflows and workflow manifests - Step configuration and execution modes - Learning, evaluation, and monitoring - Workflow-only interaction patterns such as human feedback and `todo_task` ## Files - `self_improvement_and_reporting.md` — **start here.** System overview of the self-improvement + reporting layer: the Pulse (fix) and Auto-improve (improve) loops, notifications, and the Org dashboard — how workflows self-fix toward their goals and report up so a small team can manage 100+ agents and automations - `auto_improvement_framework.md` — metric-backed workflow improvement: metrics as evidence, harden/replan decisions, schedules, and audit logs - `browser_automation.md` — durable selector and browser-discovery guidance for workflow browser steps - `cost_and_log_measurement.md` — token usage, cost files, phase/run aggregation, and log storage - `deterministic_routing.md` — route-by-file routing: deterministic switch, route file producers, and `run_workflow` `route_selections` - `evaluation_system.md` — workflow evaluation runs, eval step execution, and scoring reports - `human_feedback_system.md` — human-in-the-loop requests, UI responses, and Slack notification fallback - `iteration_run_folder_architecture.md` — run folder layout and iteration/run isolation - `learn_code_flow.md` — learn-code and code-execution modes - `learning_architecture.md` — global workflow skill, step-level learning metadata, and saved scripts - `persistent_stores_design.md` — workflow `db/`, report views, knowledgebase updates, and structured persistent outputs - `pre_validation_guide.md` — deterministic pre-validation schemas and consistency checks - `step_config_format_specification.md` — `step_config.json` schema and step-level runtime configuration - `tiered_llm_allocation.md` — tiered model selection and phase LLM allocation - `todo-task-step-type.md` — todo-task orchestration step behavior - `tool_filtering_system.md` — workflow tool filtering and runtime tool availability - `workflow_builder_commands_and_tools.md` — builder slash commands, privileged tools, and audit flows - `workflow_builder_interactive.md` — workflow phase chat and interactive builder internals - `workflow_manifest_architecture.md` — manifest format, cleaning rules, and workspace state - `workflow_monitoring.md` — run history, costs, reports, and monitoring surfaces - `workflow_scheduling.md` — scheduled workflow execution, routing, and history - `workflow_shell_working_directory.md` — shell working directories and FolderGuard scope during workflow runs ## Auto-Improvement Framework Canonical HTML: https://agentworkshq.com/docs/workflow/auto_improvement_framework/ Raw Markdown: https://agentworkshq.com/docs-content/workflow/auto_improvement_framework.md # Auto-Improvement Framework Auto-improvement gives the optimizer durable evidence and an audit trail for improving a workflow. It is **one system running at several cadences** over a **single log** (`builder/improve.html`, the **Pulse**), all sharing the same **Bug / Goal** vocabulary: - **Per-run monitor (detect, every run):** a cheap, read-only pass after each run that records whether the workflow ran correctly and whether it is achieving its goal — it never fixes anything. - **Scheduled harden (act, frequent):** applies local reliability/contract/artifact fixes for **Bug** findings. - **Scheduled replan-proposal (act, less frequent):** recommends plan/strategy changes for **Goal** gaps — it proposes, it does not auto-rewrite the plan. The model is intentionally simple: run and eval evidence are the inputs, and the optimizer chooses between hardening, replanning, eval-plan improvement, or no action. ## Two verdicts: Bug and Goal Every run is judged on two independent axes, shown as separate pills in the Pulse header — never collapsed into one "health": - **Bug** — did it *run correctly*? Errors, skipped steps, missing/empty artifacts, regressions vs the last run. Fixed by **hardening**. Roughly binary. - **Goal** — is it *achieving its success criteria*? Eval verdicts and run outputs vs `soul.md`, trending over runs via the goal card in `builder/improve.html`. Fixed by **refining or replanning**. Continuous. They are orthogonal — a run can be Bug-broken while Goal-on-target, or Bug-clean while Goal-short. **Health gates goal:** a run that wasn't operationally clean produces no trustworthy goal signal, so the goal is never judged on a broken run. For routed workflows the monitor judges **only the path that ran** — a step or eval belonging to a route this run didn't take is not-applicable, never a failure. ## Files - `soul/soul.md`: stable intent only — objective, success criteria, optional explicit user-approved constraints, and optional notification preferences. Architecture and agent assumptions are revisable and do not belong here. It stays Markdown; there is no `soul.html`. - `builder/improve.html`: the **Pulse** — the single, self-contained, human-readable HTML log and the user's primary window into the workflow. Runloop renders pending decisions first; the HTML prioritizes active challenged assumptions, today's outcome, goal progress, and recent activity, with signal/cost/maintenance detail collapsed. A bottom Agent log carries only compact handoff state and evidence pointers, never duplicate narrative. Read it before every improve pass. See `get_reference_doc(kind="review-improve-log")` for the format. - `builder/improve-archive/YYYY-MM.html`: monthly archive files for old resolved findings and routine entries. Read only the archive files referenced by the active log's archive index or an unresolved id. - `builder/card.health.html`: the compact per-run dashboard card the monitor's final notify/summary step overwrites each run (final post-Pulse status + headline/detail in `data-*` attributes). The Bug/Goal verdicts themselves live in the Pulse log's pills + goal card — there is no separate verdict file. - `route_selection.json`: which route a run took (so the monitor judges only that path). - `runs/iteration-0`: current optimizer evidence target. Old Markdown improve logs are **legacy**. Carry their unresolved findings into `builder/improve.html` as open-finding entries and stop writing Markdown logs. The old structured `improve-decision` JSON blocks and `F-`/`I-` ids are retired in favor of readable prose cards. ## Truth Hierarchy Use this hierarchy when deciding what is true: 1. `soul/soul.md`: canonical stable intent. Only explicit user-approved constraints are authoritative; architecture and agent-inferred assumptions remain challengeable. 2. `runs/iteration-0//...`: current reality from actual outputs, tool logs, validation, and eval reports. 3. `evaluation/evaluation_plan.json`: measurement definition; fix it when it conflicts with `soul.md`. 4. `planning/plan.json`: current implementation attempt, judged against `soul.md` and iteration-0 evidence. 5. `builder/improve.html` + referenced `builder/improve-archive/*.html`: memory and audit trail for past decisions, unresolved findings, deferred ideas, resolution links — and the per-criterion goal card, which is the durable goal signal over runs. ## Decision Model The per-run monitor only **detects and records**. The scheduled passes then choose one bounded action. Harden and replan are the two ends of an **exploit/explore** ladder against the success-criteria definition — same plan tools, opposite intent: - `harden_workflow(group_name?, focus?)` — **exploit: refine the current strategy.** The approach is right but execution/wiring is weak: prompts, config, validation, KB, learnings, db/report wiring, or eval coverage need repair. Not a redesign. Harden removes stale `learnings/{step-id}/main.py` for `code_exec` steps; only `learn_code` steps should retain reusable `main.py`. - Goal Advisor proposal/application — **explore: a different strategy for better success.** The current approach is **capped** — even executed cleanly it cannot satisfy the success criteria — or run evidence reveals a materially better approach. Scheduled Pulse starts this as a background `run_goal_advisor_review(...)` pass. New material strategy changes are proposal-first: create a `source="goal_advisor"` human-input request with exact intended edits, rationale, expected impact, risk, and evidence; a later Pulse pass applies approved changes with normal plan/config/eval/report tools. - Eval-plan improvement: evaluation coverage, scoring, structured output, or validation schema is weak enough that measurement cannot be trusted — a success criterion is unmeasured, an eval step is orphaned or duplicates Pulse/pre-validation, the rubric drifts, or eval cost is out of proportion to run cost. - No action: evidence is weak, recent changes need more runs, or the workflow is already aligned. Each improve pass should perform at most one primary action unless the user explicitly asks for a broader pass. ## Commands - `/define-success`: confirms the goal with the user, writes the workflow profile, and seeds the Pulse goal card from `soul.md`. - `/monitor` (and the **Monitor** toggle in the workflow toolbar): turns on the per-run review-only pass via `post_run_monitor` so every run records Bug/Goal findings. - `/improve-workflow`: reads the Pulse and current evidence, then chooses harden, replan, eval-plan improvement, or no action. - `/improve-evaluation`: improves eval coverage and rubric quality. - `/auto-improve`: turns on the per-run monitor, then creates or updates the Optimizer-mode **harden** schedule (frequent — ~every 1-2 runs) and **replan-proposal** schedule (less frequent — ~every 3-4 runs). Each Optimizer fire delegates the improvement pass to canonical `/improve-workflow` guidance, then self-tunes only its own cadence/scope. ## Audit Discipline - **Open findings** carry a short anchor id only so a later fix can close them; closing a finding edits its card in place to add a `Resolved …` line — never delete it, never open a duplicate. - **Human input requests are durable.** If Pulse or Auto Improve asks the user a question in email/chat, it first writes or refreshes a `Human input requested` card in `builder/improve.html` with question, why it matters, options, default if no answer, evidence, and status. Email is a delivery channel, not the source of truth. - **Decisions are confirmed, not assumed.** A harden/replan decision states the effect it expects when written, and stays **unconfirmed** until a later run measures it — at which point the monitor stamps the decision card once: confirmed (cite before → after), no-effect/regressed (reopen a finding), or inconclusive (the run didn't exercise the changed path). A change that quietly failed is worse than no change, so it is never hidden. - Eval-score movement is evidence, not proof. Do not claim an improvement worked until run/eval evidence supports it, and call out confounds such as small sample size, source-data drift, rubric changes, or multiple decisions in the same window. Rubric changes are the loop's biggest confound — they change what scores mean, so they go through a deliberate eval-plan-improvement pass with a major Decision card, never bundled with a harden/replan. ## Pulse Log Retention `builder/improve.html` must stay readable for users and cheap for scheduled agents to load. When it passes roughly **800 lines, 60 KB, or 20 timeline entries**, move older **resolved** findings, superseded decisions, and routine run rows into a monthly archive `builder/improve-archive/YYYY-MM.html`, leaving a one-row entry in the archive index (date range, count, any still-unresolved ids). **Never archive** open findings, user rules, current notes, the goal card, or the latest few entries — the active Pulse should always answer "what's the state of this workflow right now, and what still needs attention." Archiving is append-preserving: move old detail, leave an index row, and never rewrite the meaning of an old decision. ## Browser Automation: Durable Selectors & Discovery Canonical HTML: https://agentworkshq.com/docs/workflow/browser_automation/ Raw Markdown: https://agentworkshq.com/docs-content/workflow/browser_automation.md # Browser Automation: Durable Selectors & Discovery This doc describes the browser-specific rules injected into execution, learning, harden, and review prompts when a workflow has a browser MCP available (`playwright`, `agent-browser` skill, or a CDP port). ## The problem Browser automation agents interact with elements through two channels: 1. **Accessibility tree** — via `browser_snapshot`, which returns a YAML tree of roles + accessible names + current state (disabled, expanded, selected). Each element gets an ephemeral `ref` (`@e1`, `e68`) usable only within that snapshot. 2. **Direct DOM** — via `browser_evaluate` for reads, and the usual `browser_click` / `browser_type` / `browser_navigate` for writes. Two failure modes this doc addresses: - **Refs bleed into saved scripts.** If a learn-code step's `main.py` bakes `click @e3`, the next run's snapshot assigns `e3` to a different element and the script fails. Refs are session-local. - **The a11y tree misses elements.** Custom `
` buttons with onclick, dropdowns inside React portals, autocomplete options, form inputs lacking role/label — none show up in the snapshot. Agents flying blind either guess selectors from memory or give up. ## The core rule **Selectors persisted to main.py must be DETERMINISTIC across future runs.** A deterministic selector resolves to the same element on every replay — across browser restarts, page rebuilds, deploys that rename auto-generated classes (`css-8xy3zb`), React key changes, re-hydration. Anything that depends on session state (refs) or on unstable DOM shape (nth-child chains, auto-gen class names) is NOT deterministic and will silently match the wrong element tomorrow. Refs (`@e1`, `e68`, `"ref": "abc123"`) are session-local identifiers the browser tools assign per snapshot. They are valid only for the immediate next tool call in the same session. **Any ref hardcoded into main.py is a bug.** Two equally valid paths to a deterministic selector: - **Path A — snapshot + act.** `browser_snapshot` gives you role + accessible name + widget state. Pick a locator from the priority list, then act via individual tool calls (`browser_click`, `browser_type`, `browser_select`, `browser_navigate`) OR via Playwright code (`browser_run_code` with `page.getByRole(...)`, `page.locator(...)`). Tool-call style is a debugability preference, not a durability dimension. - **Path B — DOM probe via eval.** Run the canonical read-only probe below via `browser_evaluate` (Playwright) or `agent-browser eval` to get a structured inventory. Use when the a11y snapshot misses elements (custom `
` buttons, portal/popover children, form inputs the tree skips). Both paths terminate in a durable locator expressed in the persisted script. `browser_run_code` using Playwright's locator API (`page.getByRole('button', { name: 'Continue' }).click()`) is durable and is the right shape for chained multi-step interactions. `browser_evaluate` for inspection is allowed; `browser_evaluate` that hand-rolls `document.querySelector` for an action and then writes that selector into the script is fine IF the selector is in the durability tier below — avoid structural CSS chains that break on DOM rearrangement. ## Durable-selector priority When writing locators, agents must pick the highest-priority hook that uniquely identifies the element: 1. **`data-testid`** / `data-test` / `data-cy` / `data-qa` — ideal, rare on production sites. 2. **Hand-written semantic `id` or `name`** — e.g. `#panAdhaarUserId`, `#loginPasswordField`. **Skip auto-generated ids**: `radix-_rN_`, `mat-mdc-*`, React `:rNN:`, UUID-shaped `8-4-4-4-12` — all rotate across rebuilds. 3. **`aria-label`** — very durable when present. 4. **Role + accessible name** — `page.get_by_role("button", name="Sign in")`. 5. **`get_by_label(...)`** / `get_by_placeholder(...)` / `get_by_text(...)`. 6. **Structural CSS / XPath with nth-child chains** — last resort; flag in learnings. This priority mirrors Playwright's own recommendation but is explicitly enforced in prompts so code-gen doesn't default to fragile nth-child paths. ## The DOM probe When the a11y snapshot is insufficient (custom divs, portal dropdowns, missing form inputs), the agent runs a **read-only DOM probe** via `browser_evaluate`. The probe is a canonical JS snippet embedded in the prompt — agents copy it verbatim so results stay comparable across runs. ### What the probe returns ```jsonc { "url": "https://example.com/page", "framework": "angular-material" | "radix" | "headlessui" | "react" | "unknown", "stableHookInventory": { "data-testid": 0, "aria-label": 38, "id": 80, "name": 5, "role": 73, ... }, "popoverItems": [ // Visible children of floating/portal containers, auto-detected { "source": "popover", "tag": "div", "text": "Option A", "attrs": { "role": "option", "aria-label": "..." }, "cssPath": "[aria-label=\"Option A\"]" } ], "actionableItems": [ // Cursor-pointer / onclick / interactive-role elements body-wide { "source": "actionable", "tag": "button", "text": "Continue", "attrs": { "aria-label": "Continue", "type": "submit" }, "cssPath": "[aria-label=\"Continue\"]" } ], "counts": { "popover": 3, "actionable": 42 } } ``` ### Why each field matters - **`stableHookInventory`** tells the agent which hook strategy applies site-wide: "38 aria-labels, 0 testids → use aria-label + role+name across this workflow". Record this once per site in learnings. - **`framework`** drives known-bad-id filters (Radix → skip `_rN_`, Angular Material → skip `mat-mdc-*`). - **`popoverItems`** captures floating/portal content (React portals, Radix Popover, Headless UI menus) that the a11y tree often misses. - **`actionableItems`** catches custom `
` buttons and form inputs the a11y tree skips because the tag itself implies interactive. - **`cssPath`** is pre-filtered against auto-generated id patterns. If non-null, use it directly in main.py. If null, fall back to `role+name` from the a11y snapshot. ### When to run the probe - After first navigation to a new site (for the stable-hook inventory). - When a specific element isn't in `browser_snapshot` output but should be clickable. - When debugging a selector failure — the probe's current DOM view is ground truth. ### When NOT to run the probe - As a substitute for `browser_snapshot`. The snapshot is faster, lighter, and carries widget state (disabled/expanded). Probe the DOM only when the snapshot is insufficient. - For actions. The probe is strictly read-only. Writes go through `browser_click`, `browser_type`, `browser_select`, `browser_navigate`. ### Multi-backend invocation The probe JS body is identical across backends. Only the wrapper differs: | Backend | Invocation | |---|---| | Playwright MCP | `call_mcp('playwright', 'browser_evaluate', {'function': ''})` | | agent-browser CLI | `agent-browser eval ""` — returns JSON on stdout; pipe to a file if large | ## Shared CDP tab contract When a workflow uses `agent_browser` in CDP mode, it is controlling the user's real Chrome through a shared CDP port. Chrome tabs are global to that browser, so the workflow must not rely on agent-browser's "latest active tab" behavior. The project wrapper enforces an explicit tab on every page action. CDP is not a background-isolated mode. Because it drives a visible, user-profile Chrome window, tab selection, navigation, clicks, fills, uploads, and sometimes snapshots may bring Chrome to the foreground and interrupt typing in Codex or another app. Use CDP only when the workflow needs the user's real logged-in browser or a site blocks headless automation. For schedules and unattended work, prefer headless mode or a dedicated automation Chrome profile/port that the user does not use interactively. ### Required flow 1. Try the real tab list first. The wrapper gives it 15 seconds, then falls back to remembered selected-tab state if Chrome/CDP is stuck: ```python browser("tab", []) ``` 2. Reuse a listed or selected tab if it matches the workflow. 3. Create one stable labeled tab when no tab is selected or the selected tab is unrelated: ```python browser("tab", ["new", "--label", "upwork-profile", "https://www.upwork.com/"]) ``` 4. Navigate with URL-only `open`, then include the tab inline on later page actions: ```python browser("open", ["https://www.upwork.com/freelancers/example"]) browser("snapshot", ["tab", "upwork-profile", "-i"]) browser("click", ["tab", "upwork-profile", "@e1"]) browser("fill", ["tab", "upwork-profile", "@e2", "search text"]) browser("eval", ["tab", "upwork-profile", "document.title"]) ``` `["--tab", "", ...]` is also accepted. ### Enforcement The MCP wrapper rejects CDP page actions that omit a tab. The error includes the selected-tab hint and tells the agent to retry with either `["tab", "", ...]` or `["--tab", "", ...]`. `tab` and `reset` commands are exempt because they are used to inspect or repair tab state. The wrapper translates the inline tab into native agent-browser behavior by selecting the tab immediately before the command, then running the command under a per-CDP-port mutex. This keeps `select tab -> action` atomic enough to avoid interleaving with another workflow command on the same CDP port. Commands against different CDP ports do not share that lock. ### Important distinction Native agent-browser uses a session-level active tab (`agent-browser --session ... tab t2`, then `agent-browser --session ... snapshot`). The inline tab form is this project's MCP wrapper API; it is intentionally stricter so the LLM chooses the tab on every browser action instead of inheriting whichever tab happened to be active. ### Direct CDP scripts Raw CDP scripts can bypass the wrapper, so they must follow the same tab discipline manually: - Read existing targets from `/json/list`. - Reuse a matching target by URL/title when possible. - Prefer `agent_browser eval` with an inline tab for complex JavaScript because it uses the shared CDP command lock. - Treat direct CDP as read-only diagnostics by default. It bypasses the wrapper lock, so do not use it for navigation, clicking, filling, scrolling, uploads, or multi-page loops. - Do not call `window.location`, `element.click()`, `Target.createTarget`, `/json/new`, `Target.closeTarget`, or `/json/close` unless the task explicitly requires disposable raw-CDP control and the user accepts that it bypasses shared-browser locking. - If direct CDP is used for diagnostics, connect to the chosen target's WebSocket and close only the WebSocket client, not the Chrome tab. ## Site-access resilience Production sites often block Playwright-launched browsers. When `browser_navigate` returns "Permission Denied" / a blank page / a native `alert()` freeze, the agent should: 1. **Stop launching a fresh browser** — the Playwright fingerprint is the problem. 2. **CDP-attach to an already-running Chrome** with a real user profile: - `agent-browser --cdp ` / `--auto-connect` - Playwright's `connect_over_cdp("http://localhost:9222")` in `main.py`. 3. **Register a dialog handler** before interacting if the page shows native alerts. 4. **Document the access preamble in learnings** so future steps detect-and-switch automatically. Cloudflare-style interstitials may also be bypassable by using an alternate subdomain (e.g. `prod.gcp.example.com` vs `prod.example.com`). Record these in the site-profile learning. ## Ephemeral refs — explicit ban Refs like `@e1`, `e68`, `"ref": "abc123"` appear in snapshot output and are usable **only for the immediate next tool call in the same session**. They are: - **Not stable across snapshots** — a fresh snapshot reassigns them. - **Not stable across sessions** — every new browser spawn rebuilds the ref map. - **Never safe to save** to main.py, learnings, or any other artifact. The validator `reviewMainPyScript` at `controller_learn_code.go` runs a regex check (`Check 10`) that rejects any saved main.py containing `['"]@e\d+['"]` or `{"ref": "abc..."}` when the script calls browser tools. This catches the failure mode at save time before it costs you a broken run. ## What learnings must capture for browser steps When `learnings_access="read-write"` and the step used a browser tool, the post-step learning agent MUST produce the following content in `learnings/_global/SKILL.md` (typically split across `references/site-profile.md` and `references/selectors.md`): 1. **Site access preconditions** — CDP required, Cloudflare interstitial on apex, dialog handler needed, failure signatures so future steps can detect-and-switch. 2. **Stable-hook inventory** — once per site: counts of testids / aria-labels / ids, framework, recommended locator strategy. 3. **Per-action intents, not raw selectors** — record semantic identity plus 1–2 alternates: ``` Step [login.fill_user_id] intent: {by: "id", value: "panAdhaarUserId"} alt: {by: "placeholder", value: "PAN/ Aadhaar/ Other User ID"} alt: {by: "role+name_contains", role: "textbox", name: "User ID"} notes: Continue button stays disabled until input has a value. ``` 4. **Behavioral quirks** — multi-step flows (User ID → Continue → Password), cross-domain redirects (e-Filing → TRACES), disabled-until-valid gates, OTP/captcha branches, phantom controls (e.g. a `#btn` that looks like Proceed but does nothing — the real action is a link below). 5. **Known-bad selector patterns** — explicit per-site "do NOT use" list (Radix auto-ids, mat-mdc-*, whatever bit this workflow). Intents (not raw selectors) are the core contract: future fix loops and harden runs re-derive the selector from the intent against the current snapshot/probe output. This is why intents are 1–2 alternates per action rather than a single locator. ## Which agents see these rules The browser-authoring block (`BuildBrowserAuthoringRules`) is injected into every prompt that authors, patches, or reviews main.py — **provided the workflow has a browser MCP** (`HasBrowserCapability()` returns true): | Agent | Reads the block? | Why | |---|---|---| | Execution agent (learn-code mode) | ✅ | Writes main.py; must use durable locators | | Execution agent (code-exec-only mode) | ✅ | Even throwaway scripts shouldn't bake refs — same discipline | | Learning agent | ✅ | Needs to know what learnings SHOULD contain for browser steps | | Harden agent | ✅ | Patches main.py during eval-driven fixes; must enforce the contract | | Review agent | ✅ | Detects drift in main.py; must know what "drift" means | | Todo-task orchestrator | ✅ | Same execution semantics as regular steps | Non-browser workflows (workflow config has no browser server/skill/CDP) skip the entire block — saves ~60 lines of prompt tokens per step. ## Code locations - **`prompt_sections.go:BuildBrowserAuthoringRules()`** — the full browser-rules block including the canonical DOM probe JS. Single source of truth. - **`prompt_sections.go:browserAuthoringRulesIfBrowserEnabled()`** — helper for call sites that have direct access to the orchestrator (workshop manager). - **`prompt_sections.go:BrowserAuthoringRulesFromTemplateVars()`** — helper for call sites that only have templateVars (harden, review, execution-only). - **`controller.go:HasBrowserCapability()`** — the gate. Checks explicit non-`none` browserMode, CDP port, registered servers (`playwright`), `agent-browser` skill. Never use `GetBrowserMode() != ""` as a proxy — empty means auto-detect, not disabled. - **`execution_only_agent.go`** — template has `{{.BrowserAuthoringRules}}` slot; populated by `BrowserAuthoringRulesFromTemplateVars(templateVars)`. - **`interactive_workshop_manager.go`** — workshop/harden/review call sites set `HasBrowserAccess` from `iwm.controller.HasBrowserCapability()`. - **`learning_agent.go`** — `{{if .HasBrowserAccess}}...{{end}}` block describes the required learnings shape for browser steps. - **`controller_learn_code.go:reviewMainPyScript` Check 10** — regex-rejects ephemeral refs in saved main.py when the script imports browser tool calls. ## What's intentionally NOT done - **No new `browser_inspect` MCP tool.** The probe is JS the agent passes to existing `browser_evaluate`. Rationale: keeps tool surface lean; probe logic stays in one prompt file. Promote to a real tool only if prompt-embedded proves unreliable in practice. - **No self-healing runtime.** When a saved main.py selector rots, the current flow is: run fails → execution agent sees error → patches main.py. A future "self-healing" pass could read the intent from learnings, re-derive the selector from the current snapshot, and patch automatically without invoking the execution agent. Designed but deferred until we have real failure-pattern data. - **No iframe probe traversal.** The probe runs in the top document only. Sites with iframes (TRACES embedded forms, Stripe checkout) need per-frame probe invocation. Deferred. ## Related docs - [learning_architecture.md](learning_architecture.md) — how learnings get written and manually locked. - [step_config_format_specification.md](step_config_format_specification.md) — `learnings_access` field governing read/write of global SKILL.md. - [learn_code_flow.md](learn_code_flow.md) — the full learn-code lifecycle (fast path, fix loop, save-back). ## Cost And Log Measurement Canonical HTML: https://agentworkshq.com/docs/workflow/cost_and_log_measurement/ Raw Markdown: https://agentworkshq.com/docs-content/workflow/cost_and_log_measurement.md # Cost And Log Measurement This doc explains how workflow costs and logs are measured now. It is not the same as [workflow_monitoring.md](./workflow_monitoring.md). - `workflow_monitoring.md` is about the user-facing observability surfaces - this doc is about the storage and measurement architecture underneath those surfaces ## Two Different Systems There are two separate data planes: ### 1. Cost measurement - driven by `token_usage` events from LLM calls - persisted into the workflow `costs/` ledger - aggregated by run folder, phase, step, and model ### 2. Workflow logs - written explicitly by workflow controllers as files under `runs/.../logs/` - store execution results, conversations, validation results, learning traces, orchestration traces, and markers Costs are not reconstructed from log files. Logs are not reconstructed from cost files. ## Cost Measurement Source Of Truth The source of truth for workflow cost measurement is the `token_usage` event stream. Current flow: 1. An agent emits a `token_usage` event. 2. The context-aware bridge intercepts that event. 3. It extracts: - provider - model ID - prompt/input tokens - completion/output tokens - cache read and cache write tokens - reasoning tokens - LLM call count 4. It persists those values directly to the `costs/` ledger. This happens in [context_aware_bridge.go](../../agent_go/pkg/orchestrator/context_aware_bridge.go). ## What Gets Counted The cost ledger tracks per-model usage with pricing fields: - input tokens - output tokens - cache tokens - cache read tokens - cache write tokens - reasoning tokens - LLM call count - input cost - output cost - reasoning cost - cache read cost - cache write cost - total cost - context window usage The main types are defined in: - [base_orchestrator_types.go](../../agent_go/pkg/orchestrator/base_orchestrator_types.go) - [cost_storage.go](../../agent_go/pkg/orchestrator/cost_storage.go) Important nuance: - cache reads and cache writes are tracked separately - pricing is normalized or recomputed from model pricing helpers when files are read or merged - the ledger stores both per-model totals and per-step-per-model totals ## Cost Storage Layout The current storage layout is under `costs/`. ### Phase-only costs Used for phase agents such as planning and builder-style workflow-phase sessions. Files: - `costs/phase/token_usage.json` - `costs/phase/daily/YYYY-MM-DD.json` These are written by `PersistPhaseTokenUsage(...)` in [base_orchestrator_tokens.go](../../agent_go/pkg/orchestrator/base_orchestrator_tokens.go). Builder workflow-phase sessions also write phase cost data directly in [server.go](../../agent_go/cmd/server/server.go). ### Execution run costs Used for normal workflow execution runs. Files: - `costs/execution//YYYY-MM-DD.json` Each daily file stores a map of run folders: - `run_folders[runFolder] = TokenUsageFile` So one daily bucket can contain multiple runs for the same group on the same UTC date. ### Evaluation run costs Used for evaluation execution. Files: - `costs/evaluation//YYYY-MM-DD.json` Evaluation uses the same ledger shape as execution, but under the `evaluation` scope. ## Aggregation Shape Run-level cost data is aggregated into two main maps: - `by_model` - `by_step_and_model` `by_model`: - total usage across the whole run for each model `by_step_and_model`: - nested usage keyed by step and model - current key format is usually `phase:stepID` - older fallback format may use `phase:stepIndex` This is what lets the UI render: - stage totals - step-wise totals - model-wise totals - per-run totals ## Legacy Compatibility Older layouts still exist in some workspaces: - `runs//token_usage.json` - `evaluation/runs//token_usage.json` - workspace-root `token_usage.json` for phase costs Current behavior: - these are migrated into the `costs/` ledger when read or written - migration is handled automatically by the cost storage helpers This logic is in [cost_storage.go](../../agent_go/cmd/server/cost_storage.go) and [token_usage_store.go](../../agent_go/pkg/orchestrator/token_usage_store.go). ## How `/api/workflow/costs` Works The workflow cost API does not read a single per-run JSON file anymore. Current behavior: - reads `costs/phase/token_usage.json` - reads `costs/phase/daily/*` - reads all execution daily ledgers under `costs/execution/*` - reads all evaluation daily ledgers under `costs/evaluation/*` - merges run-folder totals across daily files - returns execution and evaluation cost data side by side for each run folder This happens in: - [workflow.go](../../agent_go/cmd/server/workflow.go) - [cost_storage.go](../../agent_go/cmd/server/cost_storage.go) ## Step Token Usage Events `step_token_usage` is a derived summary event, not the primary source of truth. Current behavior: - the system reads the persisted cost ledger for the current run - aggregates the requested step across all models - emits a summary event for UI display This means: - `token_usage` events drive persistence - `step_token_usage` events summarize persisted step totals See [base_orchestrator_tokens.go](../../agent_go/pkg/orchestrator/base_orchestrator_tokens.go). ## Workflow Log Source Of Truth Workflow logs are explicit artifact files written by workflow controllers. They are not reconstructed from the database event stream. The main log families are: - execution result JSON - execution conversation JSON - execution prompts JSON - validation JSON - pre-validation JSON - learning execution JSONL - learning conversation JSON - conditional evaluation JSON - orchestration execution JSONL - todo-task execution JSONL ## Log File Layout For a normal run folder, logs live under: - `runs/{runFolder}/logs/...` Common files: - `logs/{step-folder}/execution/execution-attempt-{N}-iteration-{M}.json` - `logs/{step-folder}/execution/execution-attempt-{N}-iteration-{M}-conversation.json` - `logs/{step-folder}/execution/execution-attempt-{N}-iteration-{M}-prompts.json` - `logs/{step-folder}/validation.json` - `logs/{step-folder}/validation-{N}.json` - `logs/{step-folder}/pre_validation.json` - `logs/{step-folder}/learning-execution.json` - `logs/{step-folder}/learning-conversation.json` - `logs/{step-folder}/conditional-evaluation.json` - `logs/{step-folder}/orchestration-execution.json` - `logs/{step-folder}/todo-task-execution.json` There are also builder/session logs under: - `builder/conversation/YYYY-MM-DD/session-{sessionId}-conversation.json` ## Who Writes The Logs Main writers: - execution result and conversation logs: [controller_execution.go](../../agent_go/pkg/orchestrator/agents/workflow/step_based_workflow/controller_execution.go) - learning logs: [controller_learning.go](../../agent_go/pkg/orchestrator/agents/workflow/step_based_workflow/controller_learning.go) - pre-validation logs: [pre_validation.go](../../agent_go/pkg/orchestrator/agents/workflow/step_based_workflow/pre_validation.go) - workflow log API scanner: [workflow.go](../../agent_go/cmd/server/workflow.go) ## Archived Logs When a step is re-executed, older log files can be archived instead of being discarded. Current behavior: - selected log files are moved into `archived/{timestamp}` under the step log folder - this preserves older attempts for debugging while keeping the active log folder readable This is handled in [controller_progress.go](../../agent_go/pkg/orchestrator/agents/workflow/step_based_workflow/controller_progress.go). ## How `/api/workflow/logs` Works The workflow log API scans the run folder and reconstructs a structured view for the UI. Current behavior: - scans `runs/{runFolder}/logs/` - scans `runs/{runFolder}/execution/` - maps step folders back to plan metadata from `planning/plan.json` - parses known file families into typed response sections - exposes conversation file paths for lazy loading This is why the execution log popup can show: - per-step execution attempts - validation history - learning traces - conditional results - orchestration and todo-task traces The implementation is in [workflow.go](../../agent_go/cmd/server/workflow.go). ## Practical Summary Use this mental model: - costs come from `token_usage` events and live in `costs/` - logs come from controller-written files and live in `runs/.../logs/` - `step_token_usage` is a derived UI summary from persisted costs - `/api/workflow/costs` merges the cost ledger - `/api/workflow/logs` scans and structures the run artifacts ## Related Docs - [workflow_monitoring.md](./workflow_monitoring.md) - [iteration_run_folder_architecture.md](./iteration_run_folder_architecture.md) - [evaluation_system.md](./evaluation_system.md) ## Evaluation System Canonical HTML: https://agentworkshq.com/docs/workflow/evaluation_system/ Raw Markdown: https://agentworkshq.com/docs-content/workflow/evaluation_system.md # Evaluation System This doc describes how workflow evaluation works now. The current system is file-backed and execution-oriented: - the evaluation plan lives in `evaluation/evaluation_plan.json` - evaluation step config lives in `evaluation/step_config.json` - evaluation execution runs in an internal sandbox under `evaluation/runs/iteration-0[/group]` - the final report is published to `evaluation/runs/{targetRunFolder}/evaluation_report.json` ## What Evaluation Is For Evaluation is the workflow's goal-measurement layer for completed execution runs. It answers: - did a target run actually satisfy the success criteria in `soul/soul.md`? - what did each evaluation step conclude, with what evidence? It is separate from: - execution-time pre-validation (mechanical run-shape checks) - Pulse per-run triage (operational breakage: errors, skipped steps, empty artifacts, hallucinated successes) - step learning - workflow execution itself Division of labor: **Pulse owns "did it run right"; evals own "did it achieve the goal."** Eval steps should map one-to-one to success criteria and never duplicate operational checks — see the `evaluation-plan` guidance template for the authoring contract. Pulse reads the eval report each run and maps verdicts onto the per-criterion goal card in `builder/improve.html`, which is the durable goal signal (there is no separate numeric metrics layer). ## Current Mental Model Use this mental model: 1. Build or edit the eval plan in `evaluation/evaluation_plan.json`. 2. Point evaluation at a completed execution run folder such as `iteration-8/production`. 3. Evaluation steps execute in an internal eval sandbox. 4. Those eval steps read the original execution artifacts through `{{TARGET_RUN_PATH}}`. 5. A scoring phase produces `evaluation_report.json`. 6. The report is published back under the requested target run folder path inside `evaluation/runs/`. ## Eval Plan Files The core evaluation files are: - `evaluation/evaluation_plan.json` - `evaluation/step_config.json` - `evaluation/eval_layout.json` - `learnings/` — shared with execution-plan learnings (see "Learnings And Step Config In Eval Mode" below) Frontend eval mode loads these files directly. The eval plan is not embedded in the main workflow manifest structure. Eval steps can be scoped to the route or artifact they apply to: ```json { "id": "eval-bid-submission", "title": "Evaluate Bid Submission", "description": "Verify bid submission artifacts...", "applies_to_routes": [ { "routing_step_id": "workflow-mode-router", "route_ids": ["route-bid"] } ] } ``` When this field is present, the runtime checks the target run's `routing-evaluation.json` before launching the eval step. Non-applicable checks are skipped and recorded in the final report with `max_score: 0`, so a run is not penalized for route paths it did not take. Current frontend behavior is implemented in [useEvaluationPlanData.ts](../../frontend/src/components/workflow/hooks/useEvaluationPlanData.ts). ## Eval Mode In The UI The UI still has a separate eval mode. Current behavior: - plan mode shows the main workflow - eval mode shows `evaluation/evaluation_plan.json` - eval steps use `evaluation/step_config.json` - eval layout is separate from the main workflow layout So evaluation is still a first-class workspace view, not just a hidden background feature. ## Running Evaluation The runtime entry point is `ExecuteEvaluationOnly(...)`. Current behavior: - reads `evaluation/evaluation_plan.json` - requires a target run folder - computes an internal eval run folder using the workshop-style `iteration-0` sandbox - sets `isEvaluationMode = true` - injects `TARGET_RUN_PATH` - applies `evaluation/step_config.json` - runs the evaluation steps - runs a final scoring phase This is implemented in [evaluation_execution.go](../../agent_go/pkg/orchestrator/agents/workflow/step_based_workflow/evaluation_execution.go). ## Internal Eval Sandbox One of the biggest architecture changes is the eval run folder model. Evaluation does **not** primarily execute inside: - `evaluation/runs/{targetRunFolder}/...` Instead it executes inside an internal sandbox: - `evaluation/runs/iteration-0` - or `evaluation/runs/iteration-0/` The target run folder is still important, but it is the thing being evaluated, not the main place eval steps execute. This is why the code uses: - internal eval run folder for execution - published target run folder for the final report ## TARGET_RUN_PATH `TARGET_RUN_PATH` is the main bridge between evaluation and the original workflow run. Current behavior: - the runtime injects `TARGET_RUN_PATH` as the absolute path to the original execution folder - eval steps are expected to read the original run artifacts through that variable For example, the original run might be: - `runs/iteration-12/production/execution` The eval step runs in the eval sandbox, but reads the original artifacts through: - `{{TARGET_RUN_PATH}}` This is the correct way to reference original execution outputs in eval steps. ## Report Phase After eval steps finish, the runtime builds a single `evaluation_report.json` covering every eval step — directly in Go (`runEvaluationReportPhase` in `evaluation_execution.go`). **There is no scoring agent, no combined LLM scoring call, and no learn_code scoring fast path** (all removed); each eval step's own structured output is its verdict. ### Report shape The on-disk report contains: - `target_run_folder`, `generated_at` - `step_scores[]`, one entry per eval step: - `step_id` - placeholder `score: 0` / `max_score: 0` with fixed `reasoning`/`evidence` text pointing readers at `output_content` ("Final scoring is disabled; this report preserves the eval step output for review") - `output_content` — the eval step's own structured output, attached from the first of: `output_content.json`, the step's `context_output` file, a `pre_validation` file, or `context_output.json` in the eval sandbox (`enrichEvaluationReportWithStepOutputs`) - eval steps skipped by `applies_to_routes` appear as skipped entries with `max_score: 0`, so a run is not penalized for route paths it did not take `output_content` is the source of truth for each eval step's verdict: the step should emit its own `score`, `max_score`, `reasoning`, and `evidence` (plus any domain-specific judgment dimensions) as structured output, enforced by the step's validation schema. Consumers — Pulse triage, the goal card in `builder/improve.html`, the scheduled improve loop, the UI — read the per-step verdicts; nothing numeric is aggregated downstream. ### Where the report lands Whichever path produces it, the runtime writes the report to: - internal path: `evaluation/runs/{internalEvalRunFolder}/evaluation_report.json` - published path: `evaluation/runs/{targetRunFolder}/evaluation_report.json` The publish step is what makes evaluation reports line up with the execution run the user asked about. ## Auto-Evaluation Evaluation can also run automatically after workflow execution. Current behavior: - after a successful batch group execution, the workflow checks whether `evaluation/evaluation_plan.json` exists - if it exists, auto-evaluation runs for that target run folder - evaluation failures do not fail the original group execution This behavior is implemented in: - [controller_batch_execution.go](../../agent_go/pkg/orchestrator/agents/workflow/step_based_workflow/controller_batch_execution.go) - [evaluation_execution.go](../../agent_go/pkg/orchestrator/agents/workflow/step_based_workflow/evaluation_execution.go) ## Evaluation Costs The old mental model of evaluation writing a standalone `evaluation/runs/.../token_usage.json` as the main source of truth is no longer the right model. Current cost architecture: - evaluation cost data is stored in the `costs/` ledger under the `evaluation` scope - `/api/workflow/costs` returns eval cost data as `evaluation_token_usage` - the UI merges execution and evaluation costs when showing workflow run cost summaries So: - execution costs -> `costs/execution/...` - evaluation costs -> `costs/evaluation/...` For the full cost architecture, see [cost_and_log_measurement.md](./cost_and_log_measurement.md). ## Evaluation Reports API The evaluation report UI is driven by `/api/workflow/evaluation-reports`. Current behavior: - scans `evaluation/runs/*` - finds `evaluation_report.json` - supports both bare iteration folders and nested group folders - returns aggregate statistics across reports - can filter to a specific run folder This is implemented in [workflow.go](../../agent_go/cmd/server/workflow.go). ## Learnings And Step Config In Eval Mode Evaluation mode has its own step config but shares the learnings namespace with execution steps. Current behavior: - step config comes from `evaluation/step_config.json` (separate from `planning/step_config.json`) - learnings go under `learnings/{stepID}/` — the same folder execution steps write to - cross-plan step-ID uniqueness between `planning/plan.json` and `evaluation/evaluation_plan.json` is enforced at write time by `validateCrossPlanStepIDUniqueness`, so the shared namespace cannot collide Eval steps therefore reuse workflow learnings (e.g. a `_global` SKILL.md written by execution steps is visible to eval steps), but keep an independent `step_config.json`. ## Validation Of The Eval Plan The system includes `validate_evaluation_plan` for checking the file after edits. Current validation checks include: - JSON parse validity - non-empty step list - unique IDs - required title and description - validation of `pre_validation` regex and JSONPath rules where present This is implemented in [evaluation_helpers.go](../../agent_go/pkg/orchestrator/agents/workflow/step_based_workflow/evaluation_helpers.go). ## What Changed From Older Docs The older evaluation doc described a different architecture in a few places. Current corrections: - evaluation is file-backed, not centered on a separate designer manager architecture - eval execution uses an internal `evaluation/runs/iteration-0[/group]` sandbox - the published report is copied to the target run folder path - evaluation costs come from the `costs/` ledger, not primarily from legacy per-run token files - the eval plan is edited and loaded directly from `evaluation/evaluation_plan.json` - the combined LLM scoring agent and the `__evaluation_scoring__` learn_code fast path were removed — the report is assembled in Go from per-step `output_content` - the numeric metrics layer (`planning/metrics.json`, `db/metrics_history.jsonl`, `propose_metric`/`retire_metric`) was removed (2026-07-01) — the goal signal is the per-criterion goal card in `builder/improve.html`, maintained agentically by Pulse from eval reports + `soul.md` ## Practical Summary Use this mental model: - edit the eval plan in `evaluation/evaluation_plan.json` - run evaluation against a completed execution run - eval steps execute in the internal eval sandbox - eval steps read original execution artifacts via `{{TARGET_RUN_PATH}}` - scoring publishes `evaluation_report.json` back to the requested target run folder - costs for eval runs are tracked in the `costs/evaluation/` ledger ## Related Docs - [cost_and_log_measurement.md](./cost_and_log_measurement.md) - [iteration_run_folder_architecture.md](./iteration_run_folder_architecture.md) - [workflow_builder_interactive.md](./workflow_builder_interactive.md) - [workflow_monitoring.md](./workflow_monitoring.md) ## Human Feedback System Canonical HTML: https://agentworkshq.com/docs/workflow/human_feedback_system/ Raw Markdown: https://agentworkshq.com/docs-content/workflow/human_feedback_system.md # Human Feedback System ## 📋 Overview The human feedback system is an interactive virtual tool that pauses LLM execution to request real-time input from users, enabling human-in-the-loop workflows. It provides browser notifications, real-time UI updates, and optional Slack integration for seamless feedback collection. The system supports 2FA/OTP input, confirmations, and clarifying questions through a thread-safe request/response coordination system. **Key Benefits:** - Enables human-in-the-loop workflows for critical decisions - Supports 2FA/OTP input, confirmations, and clarifying questions - Real-time notifications via browser push notifications - Optional Slack integration with smart delayed notifications (2-minute delay) - Thread-safe request/response coordination with timeout handling - Respond via Slack threads or UI (no UI required for Slack responses) - Integrates seamlessly with event-driven architecture --- ## 📁 Key Files & Locations | Component | File | Key Functions | |-----------|------|---------------| | **Virtual Tool** | [`human_tools.go`](../../agent_go/cmd/server/virtual-tools/human_tools.go) | `CreateHumanTools()`, `handleHumanFeedback()` | | **Backend Store** | [`human_feedback_store.go`](../../agent_go/cmd/server/virtual-tools/human_feedback_store.go) | `CreateRequest()`, `CreateRequestWithSlack()`, `SubmitResponse()`, `WaitForResponse()`, `Cleanup()` | | **API Endpoint** | [`server.go`](../../agent_go/cmd/server/server.go) | `handleSubmitHumanFeedback()` (POST `/api/human-feedback/submit`) | | **Slack Service** | [`slack_service.go`](../../agent_go/cmd/server/services/slack_service.go) | `SendFeedbackNotification()`, `GetUniqueIDFromThread()`, `TestConnection()` | | **Slack API Routes** | [`slack_feedback_routes.go`](../../agent_go/cmd/server/slack_feedback_routes.go) | Configuration and test endpoints | | **Frontend UI** | [`HumanFeedbackToolCallDisplay.tsx`](../../frontend/src/components/events/tools/ToolCallSpecialRender/HumanFeedbackToolCallDisplay.tsx) | `HumanFeedbackToolCallDisplay` component | | **Slack Config UI** | [`SlackFeedbackConfig.tsx`](../../frontend/src/components/settings/SlackFeedbackConfig.tsx) | Configuration component | | **API Service** | [`api.ts`](../../frontend/src/services/api.ts) | `submitHumanFeedback()`, `getSlackFeedbackConfig()`, `updateSlackFeedbackConfig()`, `testSlackConnection()` | | **Event Data Structures** | [`data.go`](../../agent_go/pkg/orchestrator/events/data.go) | `BlockingHumanFeedbackEvent`, `RequestHumanFeedbackEvent` | | **Orchestrator Helpers** | [`base_orchestrator.go`](../../agent_go/pkg/orchestrator/base_orchestrator.go) | `RequestHumanFeedback()`, `RequestYesNoFeedback()`, `RequestMultipleChoiceFeedback()` | --- ## 🔄 How It Works ### Core System Lifecycle 1. **Tool Registration** - `CreateHumanTools()` defines the `human_feedback` virtual tool - Tool requires: `{"message_for_user": string, "unique_id": string (UUID)}` - Executor: `handleHumanFeedback()` function 2. **LLM Invokes Tool** - LLM calls `human_feedback` with message and unique UUID - `handleHumanFeedback()` receives the call 3. **Backend Request Creation** - Global `HumanFeedbackStore` singleton creates request entry - Store maps `unique_id` → `HumanFeedbackRequest` struct - Creates channel (`chan string`) for response coordination - If Slack enabled, starts 2-minute delayed notification timer 4. **Frontend Notification** - Event system automatically notifies frontend - `HumanFeedbackToolCallDisplay` component renders - Browser push notification shown (if permissions granted) - UI displays message + text input + submit button 5. **Slack Notification (Optional, Delayed)** - If Slack enabled and user hasn't responded within 2 minutes - System checks if user already responded - If no response, sends Slack notification to configured channel - Message includes question, context, and unique request ID - Maps Slack message timestamp to unique ID for thread replies 6. **User Response** - User responds via UI (types response, clicks "Submit Feedback" or Ctrl/Cmd+Enter) - OR user replies in Slack thread - Frontend sends POST to `/api/human-feedback/submit` (UI path) - OR Slack Events API sends webhook to backend (Slack path) - Backend verifies Slack webhook signature and extracts unique ID from thread 7. **Backend Coordination** - Response written to channel - `WaitForResponse()` unblocks with user's response - `handleHumanFeedback()` returns response to LLM 8. **LLM Continuation** - LLM receives user response as tool output - Execution continues in same turn with user input ### Timeout Handling - Default timeout: **5 minutes** (tool executor) - Orchestrator helpers: **10 minutes** - On timeout: Returns error, LLM handles gracefully ### Delayed Notification Logic The Slack integration uses smart delayed notifications: 1. **Immediate**: Feedback request appears in UI right away 2. **2-Minute Delay**: System waits 2 minutes before sending Slack notification 3. **Response Check**: Before sending, system checks if user already responded 4. **Conditional Send**: Only sends Slack notification if user hasn't responded 5. **Non-Blocking**: All notification logic runs asynchronously --- ## 🏗️ Architecture ```mermaid sequenceDiagram participant LLM participant Tool as handleHumanFeedback participant Store as HumanFeedbackStore participant Slack as SlackService participant API as /api/human-feedback/submit participant Frontend as React UI participant User LLM->>Tool: Call human_feedback(message, unique_id) Tool->>Store: CreateRequestWithSlack(unique_id, message) Store-->>Tool: Request created Store->>Frontend: Event: tool_call_start Frontend->>User: Show notification + UI Store->>Store: Start 2-minute timer (async) Tool->>Store: WaitForResponse(unique_id, 5min) Note over Tool,Store: Tool blocks here alt User responds via UI (< 2 min) User->>Frontend: Type response + submit Frontend->>API: POST /api/human-feedback/submit API->>Store: SubmitResponse(unique_id, response) Store->>Tool: Channel receives response Tool-->>LLM: Return user response else User doesn't respond (< 2 min) Store->>Store: Check if user responded Store->>Slack: SendFeedbackNotification() (if enabled) Slack->>User: Slack message in channel User->>Slack: Reply in thread Slack->>Store: Extract unique_id, SubmitResponse() Store->>Tool: Channel receives response Tool-->>LLM: Return user response end ``` --- ## 🧩 Example Usage ### LLM Tool Call ```json { "tool_name": "human_feedback", "arguments": { "message_for_user": "Please enter the 2FA code sent to your email", "unique_id": "550e8400-e29b-41d4-a716-446655440000" } } ``` ### Backend Tool Handler **File:** [`human_tools.go`](../../agent_go/cmd/server/virtual-tools/human_tools.go) ```go func handleHumanFeedback(ctx context.Context, args map[string]interface{}) (string, error) { messageForUser := args["message_for_user"].(string) uniqueID := args["unique_id"].(string) feedbackStore := GetHumanFeedbackStore() if err := feedbackStore.CreateRequestWithSlack(ctx, uniqueID, messageForUser, "", nil); err != nil { return "", fmt.Errorf("failed to create feedback request: %w", err) } response, err := feedbackStore.WaitForResponse(uniqueID, 5*time.Minute) if err != nil { return "", fmt.Errorf("failed to get user feedback: %w", err) } return response, nil } ``` ### Orchestrator Helper Functions **File:** [`base_orchestrator.go`](../../agent_go/pkg/orchestrator/base_orchestrator.go) ```go // Request human feedback with text input approved, feedback, err := orchestrator.RequestHumanFeedback( ctx, "approval_123", "Please approve this plan", "Additional context", sessionID, workflowID, ) // Request yes/no feedback approved, err := orchestrator.RequestYesNoFeedback( ctx, "yesno_456", "Do you want to proceed?", "Approve", "Reject", "", sessionID, workflowID, ) // Request multiple choice feedback choice, err := orchestrator.RequestMultipleChoiceFeedback( ctx, "choice_789", "Select deployment environment", []string{"Development", "Staging", "Production"}, "", sessionID, workflowID, ) ``` --- ## ⚙️ Configuration ### Tool Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `message_for_user` | string | Yes | Message displayed to the user requesting feedback | | `unique_id` | string | Yes | Unique UUID identifying this request | ### Timeout Configuration | Component | Timeout | Location | |-----------|---------|----------| | Tool executor | `5 * time.Minute` | [`human_tools.go`](../../agent_go/cmd/server/virtual-tools/human_tools.go) | | Orchestrator helpers | `10 * time.Minute` | [`base_orchestrator.go`](../../agent_go/pkg/orchestrator/base_orchestrator.go) | | Browser notification | 30 seconds (auto-close) | Frontend component | ### Slack Configuration | Setting | Type | Description | |---------|------|-------------| | `enabled` | boolean | Whether Slack notifications are enabled | | `bot_token` | string | Slack bot token (starts with `xoxb-`) | | `app_token` | string | App-level token for Socket Mode (starts with `xapp-`) | | `channel_id` | string | Target Slack channel ID (starts with `C`) | | Notification delay | 2 minutes | Fixed delay before sending Slack notification | **Configuration Storage:** - Stored in `slack_feedback_config` database table - Managed via UI: Sidebar → Slack icon - API endpoints: `GET/POST /api/human-feedback/slack/config` ### Browser Notifications **Permissions:** - Requested on component mount - Shown when `Notification.permission === 'granted'` - Properties: Title "Human Feedback Required", body from `message_for_user`, `requireInteraction: true`, `silent: false` --- ## 🛠️ Common Issues & Solutions | Issue | Cause | Solution | |-------|-------|----------| | `timeout waiting for feedback` | User didn't respond within timeout period | Increase timeout in `handleHumanFeedback()` or orchestrator helper functions | | `feedback request already exists` | Duplicate `unique_id` used | Always generate fresh UUID: `fmt.Sprintf("feedback_%d", time.Now().UnixNano())` | | Browser notification not showing | Permission denied or not granted | Request permission via button in UI or browser settings | | Response not received | Frontend submit failed | Check browser console for errors; verify `/api/human-feedback/submit` endpoint accessible | | Tool blocks forever | Backend store channel deadlock | Check that `SubmitResponse()` is called; review channel handling in `WaitForResponse()` | | Slack connection test fails | Invalid credentials or scopes | Verify bot token (`xoxb-`), app token (`xapp-`), channel ID (`C`), bot has `chat:write` scope, bot is member of channel | | Slack notifications not sent | User responded quickly | Expected behavior - notifications only sent if no response after 2 minutes | | Thread replies not captured | Socket Mode misconfigured | Verify Socket Mode enabled, Events API enabled, `message.channels` event subscribed, App-Level Token has `connections:write` scope, bot has `channels:history` scope | --- ## 🔍 For LLMs: Quick Reference ### Constraints ✅ **Allowed:** - Any user-facing message (questions, requests for OTP/2FA, confirmations) - UUID generation for `unique_id` parameter - Waiting synchronously for user response (blocks LLM execution) - Multiple feedback requests in sequence (different unique_ids) ❌ **Forbidden:** - Reusing same `unique_id` for multiple requests - Not providing `unique_id` (required parameter) - Empty or missing `message_for_user` - Expecting instant response (users may take time) ### Example Pattern **Simple feedback request:** ```json { "tool": "human_feedback", "arguments": { "message_for_user": "Please confirm the database migration plan before I proceed", "unique_id": "feedback_1701234567890" } } ``` **2FA/OTP use case:** ```json { "tool": "human_feedback", "arguments": { "message_for_user": "Enter the 6-digit verification code sent to your email", "unique_id": "otp_1701234567891" } } ``` **LLM receives:** ``` User response: "582491" ``` ### Integration with Workflow Agents **Planning agent pattern:** ```go // 1. human_feedback - ask user for approval // 2. Receive yes/no response // 3. If approved: call update_plan_steps / add_plan_steps / delete_plan_steps ``` **Variable extraction agent pattern:** ```go // 1. human_feedback - "I detected variable {{API_KEY}}. Should I extract it?" // 2. User responds: "Yes" or "No" // 3. If yes: call update_variable tool // 4. If no: don't modify variables ``` --- ## 🔌 API Endpoints ### Submit Human Feedback **POST** `/api/human-feedback/submit` **Request:** ```json { "unique_id": "550e8400-e29b-41d4-a716-446655440000", "response": "User's feedback text" } ``` **Response:** ```json { "success": true } ``` ### Slack Configuration **GET** `/api/human-feedback/slack/config` **Response:** ```json { "enabled": true, "channel_id": "C1234567890", "bot_token": "xoxb-...1234", "app_token": "xapp-...5678" } ``` **POST** `/api/human-feedback/slack/config` **Request:** ```json { "enabled": true, "bot_token": "xoxb-...", "app_token": "xapp-...", "channel_id": "C1234567890" } ``` **POST** `/api/human-feedback/slack/test` **Response:** ```json { "success": true, "message": "Slack connection test successful!" } ``` --- ## 🔒 Security Model ### Thread Safety **Global Singleton:** ```go var ( globalHumanFeedbackStore *HumanFeedbackStore humanFeedbackStoreOnce sync.Once ) ``` **Mutex Protection:** - `sync.RWMutex` protects concurrent access to request map and waiters - Write operations (CreateRequest, SubmitResponse) use `Lock()` - Read operations (WaitForResponse lookup) use `RLock()` ### Socket Mode Security - WebSocket connection (no public webhook required) - App-Level Token separate from Bot Token - Automatic reconnection handling - Webhook signature verification for thread replies ### Token Storage - Tokens stored in database (`slack_feedback_config` table) - Tokens masked in API responses (last 4 characters only) - **Recommendation:** Encrypt tokens in production environments --- ## 📊 Database Schema ### `slack_feedback_config` Table | Column | Type | Description | |--------|------|-------------| | `id` | TEXT | Primary key (always 'slack_config') | | `enabled` | BOOLEAN | Whether Slack notifications are enabled | | `bot_token` | TEXT | Slack bot token (encrypted in production) | | `app_token` | TEXT | App-level token for Socket Mode | | `channel_id` | TEXT | Target Slack channel ID | | `created_at` | DATETIME | Creation timestamp | | `updated_at` | DATETIME | Last update timestamp | ### `slack_feedback_messages` Table | Column | Type | Description | |--------|------|-------------| | `id` | TEXT | Primary key | | `unique_id` | TEXT | Maps to `HumanFeedbackRequest.UniqueID` | | `slack_message_ts` | TEXT | Slack message timestamp | | `slack_channel_id` | TEXT | Slack channel ID | | `slack_thread_ts` | TEXT | Thread timestamp | | `created_at` | DATETIME | Creation timestamp | ### Data Structures ```go type HumanFeedbackRequest struct { UniqueID string MessageForUser string UserResponse string IsCompleted bool CreatedAt time.Time } type HumanFeedbackStore struct { requests map[string]*HumanFeedbackRequest waiters map[string]chan string mu sync.RWMutex } ``` --- ## 🚀 Slack Setup Instructions 1. **Create Slack App** at https://api.slack.com/apps 2. **Configure Bot Token Scopes**: `chat:write`, `channels:read`, `channels:history` 3. **Enable Socket Mode**: Generate App-Level Token with `connections:write` scope 4. **Enable Events API**: Subscribe to `message.channels` event 5. **Get Channel ID**: Right-click channel → View channel details 6. **Configure in UI**: Sidebar → Slack icon → Enter tokens and channel ID → Test connection → Save --- ## 📖 Related Documentation - [Workflow Monitoring](workflow_monitoring.md) - Reviews runs that may require human approval - [Todo Task Step Type](todo-task-step-type.md) - Uses human feedback for plan approval and variable confirmation - Virtual tools - See the backend tool handlers referenced above - [Event System](../core/event_system.md) - How events coordinate frontend/backend ## Learning Architecture Canonical HTML: https://agentworkshq.com/docs/workflow/learning_architecture/ Raw Markdown: https://agentworkshq.com/docs-content/workflow/learning_architecture.md # Learning Architecture This document describes the current workflow architecture as implemented now. The older model in this repo treated learning as part of a larger learning-plus-validation architecture with LLM validation modes, per-step learning files, and exploration/exploitation prompt strategies. That is no longer the right mental model. ## Current Reality Today the learning side of workflow runtime is built around two simpler ideas: - **Learning writes into a shared global skill** at `learnings/_global/`. - **Scripted code steps can also persist step-specific scripts** under their own learnings folder. If you remember the older architecture, these are the most important updates: - Learning is no longer primarily about per-step prose learnings. - The learning agent now writes domain knowledge into a global skill folder, usually centered on `learnings/_global/SKILL.md`. - For `learn_code` steps, `main.py` is the executable truth; `SKILL.md` is secondary guidance. ## Learning ### What learning writes now The main learning destination is the **global skill**: - `learnings/_global/SKILL.md` - optional supporting files under: - `learnings/_global/references/` - `learnings/_global/scripts/` - other skill-structured files `SKILL.md` should stay lean. Treat it as the index and overview for the workflow runbook, not the place for detailed accumulated guidance. Keep it under roughly 80-100 lines, with links to focused `references/.md` files. Detailed selectors, auth flows, API quirks, timing/wait rules, file-format notes, retry patterns, and step-specific HOW guidance should live in those reference files. The learning agent prompt in [`learning_agent.go`](../../agent_go/pkg/orchestrator/agents/workflow/step_based_workflow/learning_agent.go) is explicit: - accumulate **domain knowledge across all workflow steps** - keep it focused on the target system - merge findings into one shared skill - follow skill structure, not old flat learning-note files - keep `SKILL.md` as a short index and put detailed HOW knowledge in reference files The controller also hardwires global learning mode in [`controller_learning.go`](../../agent_go/pkg/orchestrator/agents/workflow/step_based_workflow/controller_learning.go): - `UseGlobalLearning = "true"` - `ContributingStepID` - `ContributingStepTitle` - optional `GlobalSkillObjective` ### Step-specific learnings still exist, but differently There are still step-specific artifacts, but they are no longer the main prose-learning model: - `learn_code` / scripted steps save reusable scripts under `learnings/{step-id}/` - especially `learnings/{step-id}/main.py` - scripted steps may also keep `SKILL.md` notes for edge cases and repair hints - metadata remains per step in `learnings/{step-id}/.learning_metadata.json` So the current split is: - **global domain knowledge** → `learnings/_global/` - **step-specific executable artifacts** → `learnings/{step-id}/` ### Learning objective The current system expects a workflow-level objective for the global skill: - `global_skill_objective` This tells the learning agent what kind of reusable knowledge should be accumulated, for example: - auth flow patterns - selectors - API patterns - common failure modes - target-system structure This is a better description of the current design than the older “extract learnings per step until stable” framing. ## Learning lifecycle ### Success learning After a successful step: - runtime can launch **success learning** in the background - it reads recent execution history and validation result - it updates the global skill - it updates step metadata This happens in [controller_learning.go](../../agent_go/pkg/orchestrator/agents/workflow/step_based_workflow/controller_learning.go). Important current details: - success learning is the real active learning path - learning detection via a separate LLM-based “did we learn something new?” phase has been removed - metadata is updated using a rule-based path instead ### Learning metadata Runtime learning metadata is observational. It gives the workflow builder and review tools evidence for deciding whether a step's learning writes are still useful, but it does not mutate `lock_learnings`. Current metadata logic in [`controller_learning_detection.go`](../../agent_go/pkg/orchestrator/agents/workflow/step_based_workflow/controller_learning_detection.go): - the step description is hashed (SHA256 of trimmed `step.GetDescription()`) on every successful run - if the hash matches the previously-stored `last_description_hash`, `description_hash_runs` increments - if the hash differs, `description_hash_runs` resets to 1 and the stored hash is updated Editing the step description resets the description-hash run counter. This is review evidence only; runtime execution does not auto-lock or auto-unlock learning. ### Manual lock lifecycle `lock_learnings` is owned by the builder/user. Set it when the workflow should keep reading the current global skill but stop accepting automatic SKILL.md writes from that step. Clear it explicitly after a material description change or when the builder/user wants that step to learn again. ### Locking and disabling learning The important current controls on `AgentConfigs`: - `learnings_access` (string enum: `"read" | "read-write" | "none"`) — primary gate. Mirrors `knowledgebase_access`. - `"read"` (default) — step sees `_global/SKILL.md` in its prompt; does NOT contribute. - `"read-write"` — step reads AND contributes. Requires `learning_objective` to be non-empty. - `"none"` — step neither reads nor contributes. The true disable. - `learning_objective` (string) — the **extraction instruction** for the post-step learning agent. Required when access is `"read-write"`. No longer a gate. - `lock_learnings` (bool) — freezes the learning agent for this step even while access is `"read-write"`. Existing `SKILL.md` still flows into execution prompts. Runtime never auto-sets or auto-clears this; it is a builder/user decision. - `global_skill_objective` (workflow-level, not per-step) — describes what domain knowledge the global skill should accumulate. Auto-migration for legacy configs (runtime-only, no file rewrites): if `learnings_access` is unset, `learning_objective` non-empty infers `"read-write"`; empty infers `"read"`. Recommended usage: - leave `learnings_access` unset (defaults to `"read"`) for most steps — they benefit from cross-step context. - set `learnings_access: "read-write"` + a non-empty `learning_objective` on steps that produce durable HOW-knowledge about the target system. - set `learnings_access: "none"` for steps that are truly throwaway or whose context would pollute the global skill (e.g. pure file moves, human-input steps — the latter is forced to `"none"` automatically). - set `lock_learnings: true` only when the builder/user intentionally decides the step should stop writing SKILL.md; include `review_notes` explaining why. ### Failure learning The older docs described a full failure-learning architecture. That is not a good description of the current codebase. What still exists: - some comments, metadata fields, and workshop text still reference failure learning What matters operationally now: - the active, clearly implemented learning path is success learning into the global skill Until failure-learning behavior is re-established as a first-class runtime path, docs should not present it as a central architecture pillar. ## Scripted code steps For `learn_code` steps, learning and execution are intentionally split: - `main.py` is the executable artifact - `SKILL.md` is supporting knowledge - global skill captures reusable domain knowledge - step folder captures the reusable script and related metadata That means learning for scripted steps is not just “write prose notes.” It is: - maintain reusable code in `learnings/{step-id}/main.py` - maintain reusable domain knowledge in `learnings/_global/` See [learn_code_flow.md](learn_code_flow.md). ## Current file layout ### Global ```text learnings/ _global/ SKILL.md references/ scripts/ ``` ### Step-specific ```text learnings/ / .learning_metadata.json SKILL.md # optional supporting notes main.py # scripted steps scripts/ diffs/ ``` Not every step uses every file. The important distinction is: - `_global/` is the shared workflow skill - `/` is the step-specific artifact area ## What to update in other docs When editing related workflow docs, keep these rules consistent: - describe learning as global-skill-first - gate read access and write contribution through `learnings_access` — `lock_learnings` is a freeze switch, not the enable/disable mechanism - for scripted steps, describe `main.py` as the executable source of truth - description-hash metadata is review evidence only; runtime does not auto-lock or auto-unlock learnings - leave validation details to the dedicated pre-validation docs ## Code references - [`controller_execution.go`](../../agent_go/pkg/orchestrator/agents/workflow/step_based_workflow/controller_execution.go): learning triggers and post-execution flow - [`controller_learning.go`](../../agent_go/pkg/orchestrator/agents/workflow/step_based_workflow/controller_learning.go): success learning and global-skill write path - [`learning_agent.go`](../../agent_go/pkg/orchestrator/agents/workflow/step_based_workflow/learning_agent.go): global skill prompt and skill-structured output - [`controller_learning_detection.go`](../../agent_go/pkg/orchestrator/agents/workflow/step_based_workflow/controller_learning_detection.go): learning metadata updates - [`interactive_workshop_manager.go`](../../agent_go/pkg/orchestrator/agents/workflow/step_based_workflow/interactive_workshop_manager.go): current user-facing guidance for global learning ## Related docs - [pre_validation_guide.md](pre_validation_guide.md) - [step_config_format_specification.md](step_config_format_specification.md) - [learn_code_flow.md](learn_code_flow.md) ## Pulse: post-run pipeline consolidation Canonical HTML: https://agentworkshq.com/docs/workflow/pulse_consolidation/ Raw Markdown: https://agentworkshq.com/docs-content/workflow/pulse_consolidation.md # Pulse: post-run pipeline consolidation Status: **Implemented** (2026-06-21) — Phase 1 (rename) + Phase 2 (backup-always + Pulse-does-low-risk-fixes). Auto-fix is intentionally scoped to low-risk reversible harden; bigger `replan` changes stay with the scheduled auto-improve loop. ## Pulse vs the auto-improve loop (division of labor) These are two tiers over the same Pulse log / Bug-Goal vocabulary — they compose, they don't overlap: - **Pulse** — runs after **every** run (when enabled). Cheap + immediate: **back up → triage → apply a low-risk reversible Bug harden → record a Goal replan *proposal* → notify.** Never runs `replan` or a full improvement pass itself. - **Auto-improve loop** — a **separate schedule** (`/auto-improve`, optimizer mode). Owns the **bigger changes**: batched harden and the **`replan`** tool for structural plan rewrites, acting on the proposals Pulse recorded. Pulse is skipped after these optimizer-mode runs (`scheduler.go`: `WorkshopMode != "optimizer"`), so the two never fight over the same run. ## Problem After a run there are **four disconnected mechanisms**, with different triggers, gating, and reliability: 1. **Post-run monitor** (`runPostRunMonitor`, `scheduler.go:1164`) — opt-in (`post_run_monitor`), a dedicated agent pass that writes the Pulse log (`builder/improve.html` verdict pills + goal card). Its final notify/summary step writes the `builder/card.health.html` dashboard card after harden, artifact review, cost/time, backup, and publish are known. Auto-improve **cadence #1**. 2. **Scheduled harden** — auto-improve **cadence #2**, applies low-risk Bug fixes on its own schedule. 3. **Scheduled replan-proposal** — auto-improve **cadence #3**, proposes Goal changes. 4. **Backup** (`workflowRunBackupDirective`, `background_agents.go:1607`) — a directive appended to the builder's AUTO-NOTIFICATION after a `run_full_workflow` completion. Best-effort steering, **not** a guaranteed step; scheduled runs don't back up themselves (`scheduler.go:1268`). Naming reality: the monitor reference doc already calls `builder/improve.html` "**the Pulse log**". So "monitor" (the pass) and "Pulse" (the log) are two halves of one feature that was never unified in the UI. ## Decisions (user, 2026-06-21) 1. **Pulse everywhere.** The feature/toggle is named **Pulse**. The right-panel tab is **Pulse** (reverts the Phase-3 "History" rename). Internally the monitor pass becomes the "Pulse pass." 2. **Full auto.** Enabling **Pulse** runs the complete post-run loop **every run**: **back up → triage → apply fixes (harden for Bug, replan for Goal)**. The four mechanisms above collapse into this one toggle. ## Target model ``` Pulse (one toggle) → after every run: 1. Back up — always, guaranteed (local-git default = zero-config). Skipped only when source_hash is unchanged (no empty commits). 2. Triage — the current monitor: Bug + Goal verdicts, Pulse log, verdict signal. 3. Fix — Bug → harden (low-risk reliability/contract fixes); Goal → replan (now applied, was propose-only). 4. Notify — one transition notification (unchanged policy). Pulse tab (UI) → the durable record: Timeline (improve.html) + Plan edits (changelog). ``` Backup is no longer a separate steering directive; it's step 1 of the Pulse pass and runs in the scheduler post-run block where the monitor already lives. ## Changes required ### UI (rename — safe, mechanical) - `WorkflowCanvas.tsx`: tab label + titles "History" → **"Pulse"** (revert Phase 3). - `HistoryView.tsx` → `PulseView.tsx` (component rename; keeps Timeline + Plan edits sub-tabs). `PlanChangelogFeed.tsx` unchanged. - `WorkflowToolbar.tsx`: the monitor toggle (`monitorOn`) relabels to **"Pulse"**. ### Behavior (the real change — needs care) - `runPostRunMonitor` → `runPulse` (or keep name, expand prompt). Add **backup as step 1** (guaranteed, source-hash gated) and **fix as step 3** (harden + replan). - Rewrite `guidance/templates/system/post-run-monitor.md`: it is no longer read-only — it backs up, then triages, then applies the safe fixes. Reconcile with the existing harden / replan reference docs so there's one fix contract, not three. - `scheduler.go`: backup runs here for scheduled runs (always). Remove / demote the `workflowRunBackupDirective` steering path so backup isn't double-driven. - `PostRunMonitor` manifest flag stays the gate but is surfaced as "Pulse enabled". ### Risk / open - **Auto-fix every run changes the safety model.** Cadences #1–#3 were deliberately separated (cheap read-only triage vs riskier fixes on a slower schedule). Folding them means a fix can land on every run. Mitigation: keep harden's "low-risk only" contract; keep replan as the heavier action and decide whether it truly applies or still proposes for high-risk plan rewrites. **To confirm before the behavioral rewrite ships.** - Cost: every run now does backup + triage + (sometimes) fix instead of a cheap triage. Acceptable per the "full auto" decision, but worth a source-hash / no-op fast path. ## Backup visibility (2026-06-21) Backup surfaces in three places now: the toolbar status dot + the Backup popup (existing), and — added here — the **Pulse log Run row**. Since Pulse owns the backup, its step-3 Run row records the backup result (`backed up ✓ ` / `unchanged — already backed up` / `backup ✗ `). Doc-only change to `post-run-monitor.md` (step 3) and `review-improve-log.md` (Run kind) — agent-driven, no Go. ## Phasing 1. **Rename to Pulse** ✅ Done (2026-06-21, UI only). `WorkflowCanvas` tab + titles "History" → "Pulse"; `HistoryView.tsx` → `PulseView.tsx`; toolbar "Monitor" button + help popup → "Pulse" (internal vars `monitorOn`/`post_run_monitor` unchanged). Verified: tsc 0, lints clean. 2. **Backup always + Pulse does low-risk fixes** ✅ Done (2026-06-21). Rewrote the Pulse pass prompt in `scheduler.go` (`runPostRunMonitor`) to the 4-step contract (back up → triage → low-risk harden / replan-proposal → notify) and rewrote `guidance/templates/system/post-run-monitor.md` to match (new "0. Back up first" + "3b. Apply the fix" sections; dropped the strict read-only framing; kept step 5 = Notify so the prompt's reference still resolves). Backend build + vet OK. Safety rails kept: **low-risk reversible fixes only** (bigger work → auto-improve loop), and **source-hash gate** so steady runs skip the push. 3. **Unify the backup directive** ✅ Done (2026-06-21). `workflowRunBackupDirective` (the interactive arm) now shares **one backup contract** with Pulse's step 1: same zero-config local-git default, same source-hash skip. So the two arms (Pulse = scheduled, directive = interactive + Pulse-off fallback) can't double-push — whichever runs second sees the source already backed up and skips. Stale `scheduler.go:1268` comment corrected. Build + vet OK; no test pinned the old wording. **Non-goal (decided 2026-06-21):** do NOT move the source-hash skip into deterministic Go code. The whole post-run/backup loop stays agent-driven — the agent reads `backup/status.json` and decides. No Go-side gating coupling. ## Phase 4 — Publish folded into the Pulse loop (2026-06-24) Publish (the public-URL twin of Backup — see `docs/workflow/publish_design.md`) is now a step of the Pulse pass, so a workflow's public dashboard stays current automatically: ``` Pulse → after every run: 1. Back up — guaranteed, source-hash gated (unchanged). 2. Triage — Bug + Goal verdicts, Pulse log (unchanged). 3. Fix — low-risk harden / replan proposal (unchanged). 4. Re-publish — ONLY if publish is configured + enabled; rebuilds from source and redeploys both artifacts. Skipped when publish is off. 5. Notify — one transition notification (unchanged). ``` `post-run-monitor.md` gained a "### 4b. Re-publish (only if publish is on)" section. Like backup, publish is **agent-driven and read-only in the UI**: setup/run/restore/publish all happen in the builder chat via the **`/backup`** and **`/publish`** slash commands; the toolbar popups are status-only. The dead write endpoints (`/workflow/{backup,publish}/{config,run}`) were removed. ### Publish output contract (what `/publish` ships) - **Both artifacts, always** — the baked report **dashboard** (`dashboard.html`) AND the **Pulse log** (`pulse.html`, from `builder/improve.html`), joined by a top-nav `index.html` wrapper (Dashboard | Pulse tabs). Publishing only one is a bug. - **Dark only, matching the app** — the published pages must set **both** theming hooks on ``: `class="dark"` (the app's Tailwind mechanism, `ThemeContext`) **and** `data-theme="dark"` (what report widgets `HtmlWidgetFrame` and the Pulse-log skeleton key on). Setting only `data-theme` left the dashboard light, because report widgets key primarily off the `.dark` class. No toggle, no `prefers-color-scheme` (that follows the viewer's OS). See `[[project_published_page_theme_contract]]`. - **Stage outside the workspace** — deploy CLIs (`netlify`, `vercel`, …) write state (`.netlify/`, `.vercel/`) to their CWD. The workflow folder is writable EXCEPT `planning/`, but the CLI's CWD is often the docs root (above the folder, outside the write allow-list), so it gets rejected. Copy the finished static files to `/tmp/publish-/` and run the deploy from there. ### Plan-edits consolidation (2026-06-24) The toolbar **Plan edits** popup (the granular `planning/changelog/*.json` audit feed) got a **Consolidate** control — drop edits older than 7/30/90/180 days — backed by `POST /workflow/plan-changelog/prune`. The server prunes (deletes whole `changelog-*.json` files older than the cutoff) because `planning/` is shell-guarded from the agent. ## Workflow self-improvement & reporting — system overview Canonical HTML: https://agentworkshq.com/docs/workflow/self_improvement_and_reporting/ Raw Markdown: https://agentworkshq.com/docs-content/workflow/self_improvement_and_reporting.md # Workflow self-improvement & reporting — system overview **Start here.** This is the map of how a workflow keeps itself healthy and moving toward its goals, and how that work is made visible and steerable for the user. It ties together five parts that are otherwise documented separately: **Pulse**, **Auto-improve**, the **scheduled step messages**, **notifications**, and the **Org dashboard**. Each section links to the detailed doc. ## Why this is the critical layer — managing at scale This is the most important subsystem in the product: it's what makes **100+ agents and automations manageable** by a small team. Without it, every workflow needs a human watching it — which caps you at a handful. With it: - each workflow **self-heals** (Pulse fixes operational breakage) and **self-improves toward its goal** (Auto-improve), so routine operation needs no human; - the reporting **rolls everything up** so a human manages **by exception** — the dashboard's triage bar surfaces only what needs attention, notifications fire only on real transitions (broke / recovered / new finding), and big changes wait as **proposals to approve**. You take in a hundred automations at a glance and act only where the system flags it. Span of control is the whole point: the system handles the routine fixing and improving and surfaces only the **exceptions + decisions**, so human effort scales with the number of *exceptions*, not the number of *workflows*. That is the difference between running 5 automations and running 100+. ## 1. Purpose — two jobs 1. **Fix/improve workflows toward their goals.** Keep them *working*, and move them toward *winning*. 2. **Make that legible and steerable.** Report what's happening so the user can *see*, get *alerted*, and *decide* — because the system proposes big changes rather than applying them silently. Everything below is one of those two jobs, or the substrate that connects them. ## 2. The two loops Two loops run per workflow. They answer different questions and own different halves of a workflow's state. | | **Pulse** | **Auto-improve** | |---|---|---| | Job | **FIX** — keep it *working* | **IMPROVE** — make it *win* | | Axis | 🩺 operational + 💵 spend/time ("does it run right, and did it spend sanely?") | 🎯 goal ("is it achieving its goal?") | | Trigger | **after every run** (reactive) | **scheduled** (proactive) | | Autonomy | applies low-risk fixes itself (`harden_workflow`) | **proposal-only** for big changes (replan); user/builder approve | | Statuses | healthy / bug / critical; normal / elevated / missing cost | on-track / at-risk / off-goal | | Code | `runPostRunMonitor` / `postRunMonitorSteps` (`scheduler.go`) | `optimizerScheduleMessages` (`scheduler.go`) | | Guidance | `post-run-monitor.md`, `optimize-playbook.md` | `optimize-playbook.md` + `get_workflow_command_guidance(kind="improve-workflow")` | | Detailed doc | `pulse_consolidation.md` | `auto_improvement_framework.md` | **Pulse step sequence** (one focused turn per step): triage → fix/harden → artifact review → LLM/cost report → backup → publish → notify. **Auto-improve step sequence:** pre-backup → improve → final backup → publish → notify. The improve turn can also **adjust its own cadence** (run more often while actively improving, back off when stable) via `update_workflow_schedule` — see `workflow_scheduling.md`. "Working but off-goal" is a normal, important state: Pulse says it runs fine, Auto-improve says it isn't moving the goal yet. ## 3. The shared substrate (the loops' memory) Both loops read and write **`builder/improve.html`** — the per-workflow Pulse/improve log, newest-first. It *is* the loop's memory: - **Verdict pills** (Bug, Goal), stamped with the run they're as-of. - **Goal card** — each success criterion's Met/Short/At-risk + evidence. - **Decision cards** — each fix (harden/replan) the loop applied. - **Auto-improve major decision cards** — visually distinct decision entries with `Why now`, evidence, change, expected impact, files touched, and remaining risk/gap, so material replans/report/eval/cadence changes do not look like routine Pulse notes. - **Self-verification** — on a later run the loop *confirms the last unconfirmed Decision*: `ok` (cite before→after), `bad` (regressed → reopen a finding), or `flat` (path not hit → stays pending). So "I fixed X last run → re-check X" is built in. - **Open findings** — anchored ids that persist across runs until a fix closes them. - **Human input requests** — structured question cards with status, options/default, and evidence; notifications may point to them, but email is not the source of truth. See `review-improve-log.md` for the log's structure and the confirm-Decision rules. Each loop also writes a compact **dashboard card** in the workflow's own workspace, every run (overwrite), via current workspace write paths such as `diff_patch_workspace_file`: - Pulse final notify/summary step → `builder/card.health.html` (🩺 final post-Pulse status + compact named fields for state/fix/evidence/next, not the full email narrative) - Pulse report step → `builder/card.cost.html` (💵 cost/time status + headline/metric) - Auto-improve → `builder/card.progress.html` (🎯 status + goal + headline) These are served to the UI by `getBuilderDoc(workspace, "card-health"|"card-progress"|"card-cost")` (`auto_improvement_endpoints.go`). ## 4. The reporting / steering surfaces The same verdicts/Decisions/cards the loops produce while fixing are what surface here — *#2 is #1 seen again*, never a separate analysis. - **Notifications — `notify_user`** (active, "you need to know this"). Fans out to connected channels: **Gmail** (`email_html` rich body + `email_body` plain fallback — must be **inline-styled**, Gmail strips `