diff --git a/agents/docs/state.md b/agents/docs/state.md index 5bb989ef..f7a46f56 100644 --- a/agents/docs/state.md +++ b/agents/docs/state.md @@ -1,14 +1,14 @@ --- -last_checked_commit: eed88a1 -last_run: "2026-07-10T23:17:20Z" -docs_gaps_found: 7 -branches_created: ["docs/auto-update-2026-02-21", "docs/auto-update-2026-03-01", "docs/auto-update-2026-03-05", "docs/auto-update-2026-03-07", "docs/auto-update-2026-03-13", "docs/auto-update-2026-06-30", "docs/auto-update-2026-07-10"] +last_checked_commit: 0c0a1b3 +last_run: "2026-10-26T07:09:34Z" +docs_gaps_found: 6 +branches_created: ["docs/auto-update-2026-02-21", "docs/auto-update-2026-03-01", "docs/auto-update-2026-03-05", "docs/auto-update-2026-03-07", "docs/auto-update-2026-03-13", "docs/auto-update-2026-06-30", "docs/auto-update-2026-07-10", "docs/auto-update-2026-10-26"] status: completed --- # Documentation Audit State -**Last Updated:** 2026-07-10 +**Last Updated:** 2026-10-26 This document tracks the state of the documentation audit agent, enabling incremental reviews that analyze only new commits since the last check. @@ -19,10 +19,10 @@ incremental reviews that analyze only new commits since the last check. | Metric | Value | Notes | |--------|-------|-------| -| Last checked commit | eed88a1 | chore: version packages (#318) | -| Last run | 2026-07-10T23:17:20Z | Manual invocation | -| Gaps found (last run) | 7 | openChatSession, session reaper + wakes, trigger fork, cancelJob interrupt, translator attribution, ChatMessage.uuid, stale What's New | -| Branches created | docs/auto-update-2026-07-10 | Streaming sessions + reaper/wake docs, fork/cancelJob/uuid reference updates, What's New refresh | +| Last checked commit | 0c0a1b3 | chore: version packages (#478) | +| Last run | 2026-10-26T07:09:34Z | Automated audit | +| Gaps found (last run) | 6 | Session adoption, job usage/tokens, inline images, runtime schedules, spawned trigger | +| Branches created | docs/auto-update-2026-10-26 | Session adoption API, job usage tracking, inline images, schedule mutation recipe, spawned trigger type | --- @@ -30,6 +30,7 @@ incremental reviews that analyze only new commits since the last check. | Date | Commits Analyzed | Gaps Found | Action | Branch | |------|-----------------|------------|--------|--------| +| 2026-10-26 | 85 | 6 | created-branch | docs/auto-update-2026-10-26 | | 2026-07-10 | 51 | 7 | created-branch | docs/auto-update-2026-07-10 | | 2026-06-30 | 20 | 3 | created-branch | docs/auto-update-2026-06-30 | | 2026-03-13 | 3 | 5 | created-branch | docs/auto-update-2026-03-13 | diff --git a/docs/src/content/docs/architecture/job-system.md b/docs/src/content/docs/architecture/job-system.md index 9e52c9e7..4d7ebdbe 100644 --- a/docs/src/content/docs/architecture/job-system.md +++ b/docs/src/content/docs/architecture/job-system.md @@ -95,6 +95,7 @@ output_file: job-2024-01-19-abc123.jsonl | `prompt` | `string?` | The prompt given to the agent | | `summary` | `string?` | Brief summary of job results (extracted from the final assistant message) | | `output_file` | `string?` | Filename of the JSONL output log | +| `usage` | `object?` | Token usage and cost data (see [Usage and Cost Tracking](#usage-and-cost-tracking)) | ### Immutable Fields @@ -375,6 +376,79 @@ The cache needs no explicit invalidation — it revalidates against the filesyst This mirrors the incremental `AttributionIndexBuilder` used by session discovery, with one difference: the attribution index is TTL-based, while the job index is validated against file mtime and size. +## Usage and Cost Tracking + +Every completed job now persists detailed token usage data in the `usage` field of its metadata file. This field captures per-model token counts, turn count, and the SDK's authoritative cost estimate, providing complete visibility into resource consumption for every agent run. + +### What's Tracked + +The `usage` field contains: + +| Field | Type | Description | +|-------|------|-------------| +| `per_model` | `Record` | Token counts broken down by model (e.g., `claude-opus-4-8`, `claude-haiku-4`) | +| `num_turns` | `number?` | Total agentic turns reported by the runtime | +| `total_cost_usd` | `number?` | SDK-authoritative run cost in USD (absent for CLI/Max-plan runs) | + +Each `ModelTokenUsage` entry contains four token classes: + +| Token Class | Description | +|-------------|-------------| +| `input_tokens` | Fresh (non-cached) input tokens | +| `output_tokens` | Generated output tokens | +| `cache_creation_input_tokens` | Tokens written to the prompt cache | +| `cache_read_input_tokens` | Tokens read from the prompt cache | + +### Why Per-Model? + +A single job can use multiple models — for example, an Opus agent delegating to Haiku sub-agents via the Task tool. The `per_model` breakdown ensures you can track which model consumed which tokens, enabling accurate cost attribution even when runs span multiple models. + +### Example Usage Data + +```yaml +usage: + per_model: + claude-opus-4-8: + input_tokens: 12543 + output_tokens: 3421 + cache_creation_input_tokens: 45023 + cache_read_input_tokens: 38201 + claude-haiku-4: + input_tokens: 2341 + output_tokens: 892 + cache_creation_input_tokens: 0 + cache_read_input_tokens: 0 + num_turns: 14 + total_cost_usd: 0.234 +``` + +### Accessing Usage Data + +The usage field is available on every `JobMetadata` object returned by: + +- `FleetManager.getJob(jobId)` +- `FleetManager.getJobs(filters)` +- `JobManager.getJob(jobsDir, jobId)` +- `JobManager.listJobs(jobsDir, filters)` + +```typescript +const job = await manager.getJob('job-2024-01-19-abc123'); +if (job.usage) { + console.log(`Total turns: ${job.usage.num_turns}`); + console.log(`Total cost: $${job.usage.total_cost_usd}`); + + for (const [model, tokens] of Object.entries(job.usage.per_model)) { + console.log(`${model}: ${tokens.input_tokens} input, ${tokens.output_tokens} output`); + } +} +``` + +### Backward Compatibility + +The `usage` field is **backward-compatible** — job records written before this field existed will have `usage: null` or `undefined`. Always check for the field's presence before accessing its contents. + +herdctl deliberately **does not persist a $/token price table** — prices drift over time, and Max/CLI plan users have no real per-token spend. The consuming application should apply its own pricing logic if needed. The `total_cost_usd` field is the SDK's own reported figure, not a herdctl-computed price. + ## Job Events The FleetManager emits strongly-typed events throughout a job's lifecycle. These events drive the web dashboard, CLI output, and any programmatic subscribers. diff --git a/docs/src/content/docs/concepts/sessions.md b/docs/src/content/docs/concepts/sessions.md index 33734b14..71261546 100644 --- a/docs/src/content/docs/concepts/sessions.md +++ b/docs/src/content/docs/concepts/sessions.md @@ -465,10 +465,91 @@ Every discovered session is attributed to an **origin** that describes how it wa | `discord` | Started by the agent's Discord bot | | `slack` | Started by the agent's Slack bot | | `schedule` | Started by a scheduled job (`trigger_type: schedule`) | +| `adopted` | Pre-existing session explicitly claimed via the adoption API | | `native` | Started by running `claude` directly in the terminal, in a working directory that an herdctl agent also uses. Jobs with other trigger types (manual, webhook, chat, fork) are also grouped here. These sessions are discovered and displayed but not managed by herdctl. | Attribution is determined by cross-referencing herdctl's job metadata files and the platform session records under `.herdctl/`. A native session can also be continued interactively from the web dashboard ("ad hoc" continuation) — herdctl then creates a new interactive session that picks up the conversation. +## Session Adoption + +The session adoption API allows you to claim pre-existing Claude Code sessions—conversations started outside herdctl—and make them discoverable, attributed, and resumable under a specific agent. This is useful when you have existing Claude Code terminal sessions for a project and want to bring them into herdctl for visibility and management. + +:::note[Library API Only] +Session adoption is currently a **library API only** — there is no `herdctl sessions adopt` CLI command. Use `FleetManager.adoptSession()`, `adoptSessionsFrom()`, or `listAdoptableSessions()` programmatically. +::: + +### Why Adopt Sessions? + +When you run `claude` directly in your terminal before setting up herdctl, those sessions are stored in `~/.claude/projects/` but have no association with any herdctl agent. Session adoption: + +- Makes those conversations **visible** in the web dashboard under the agent +- Preserves full **conversation history** for future reference +- Enables **resuming** those sessions as if they were started by herdctl +- Provides **proper attribution** (origin: `adopted`) so you can distinguish imported vs. new sessions + +### Listing Adoptable Sessions + +The `listAdoptableSessions()` method scans a working directory's Claude Code transcript folder to find sessions that can be adopted: + +```typescript +const adoptable = await manager.listAdoptableSessions('my-agent'); + +for (const session of adoptable) { + console.log(session.sessionId); + console.log(` Auto-name: ${session.autoName}`); + console.log(` Modified: ${session.mtime}`); +} +``` + +This returns only **adoptable** sessions — native, non-sidechain transcripts that are not already adopted and not attributed to a real run. See [`listAdoptableSessions()` in the FleetManager reference](/library-reference/fleet-manager/#listadoptablesessionsname-fromworkingdir) for details. + +### Adopting a Single Session + +Use `adoptSession()` to claim one session by ID: + +```typescript +await manager.adoptSession('my-agent', '0f3c8b2a-...'); +``` + +This records an adoption claim in `.herdctl/adopted-sessions/` and makes the session appear under the agent with `origin: "adopted"`. **No files are moved** — the transcript stays where it is. + +### Bulk Adoption + +The `adoptSessionsFrom()` method adopts every adoptable session found in a directory, optionally moving or linking the transcript files into the agent's own folder: + +```typescript +const result = await manager.adoptSessionsFrom('my-agent', { + fromWorkingDir: '/Users/ed/Code/myproject', + mode: 'copy', // 'copy' | 'move' | 'link' + dryRun: false, +}); + +console.log(`Adopted ${result.adopted.length} sessions`); +for (const skip of result.skipped) { + console.warn(`Skipped ${skip.sessionId}: ${skip.reason}`); +} +``` + +Placement modes: + +| Mode | Effect | +|------|--------| +| `copy` (default) | Duplicate the transcript file, preserving the original | +| `move` | Relocate the file (disappears from the user's terminal history) | +| `link` | Hard-link (same inode, so resumes append to the original too) | + +See [`adoptSessionsFrom()` in the FleetManager reference](/library-reference/fleet-manager/#adoptsessionsfromname-opts) for full details. + +### Releasing an Adoption + +The `unadoptSession()` method releases a session this agent had adopted: + +```typescript +const released = await manager.unadoptSession('my-agent', '0f3c8b2a-...'); +``` + +This removes the adoption record only — **the transcript stays on disk**. The session reverts to an ordinary unattributed native session: visible in all-sessions views, invisible under the agent. + Attribution matters because it tells you at a glance whether a session is something herdctl is responsible for or something a developer started independently. In the dashboard, sessions are labeled with their origin so you can filter and distinguish between managed and unmanaged activity. ## Sidechain Sessions diff --git a/docs/src/content/docs/concepts/triggers.md b/docs/src/content/docs/concepts/triggers.md index 9ea86e2e..bbbe1807 100644 --- a/docs/src/content/docs/concepts/triggers.md +++ b/docs/src/content/docs/concepts/triggers.md @@ -15,6 +15,7 @@ Every schedule needs a trigger to determine when it runs. The trigger type you c | **Cron** | Precise timing (daily reports, scheduled maintenance) | Available | | **Webhook** | Event-driven automation (deployments, external events) | Future | | **Chat** | Interactive responses (support, commands) | Available | +| **Spawned** | Sub-agent jobs created via the Task tool | System-generated | ## Interval Triggers @@ -456,6 +457,31 @@ Users can ask the bot questions like "What's the current price?" between schedul - Background automation (use cron or interval) - Event-driven workflows (use webhook when available) +## Spawned Trigger Type + +The `spawned` trigger type is a system-generated marker for jobs created by sub-agents via the Task tool. You cannot create a schedule with `type: spawned` — this trigger type exists only as job metadata to record provenance. + +When an agent uses the Task tool to spawn a sub-agent, herdctl creates a job with `trigger_type: spawned`. This allows you to: + +- **Filter jobs** by spawn origin in job listings +- **Track delegation** patterns — which agents spawn sub-agents and how often +- **Distinguish** between user-initiated and agent-initiated work + +The `spawned` type is an enum value in the job metadata schema, ensuring type safety and enabling downstream consumers (like dashboards or analytics tools) to distinguish spawned jobs from scheduled, manual, or chat-triggered runs. + +### Example Job Metadata + +```yaml +id: job-2024-01-19-xyz789 +agent: my-subagent +trigger_type: spawned # Set by the Task tool +status: completed +started_at: "2024-01-19T10:30:00Z" +finished_at: "2024-01-19T10:32:15Z" +``` + +This trigger type is purely informational — it has no scheduling logic of its own. + ## Choosing the Right Trigger ### Decision Guide diff --git a/docs/src/content/docs/configuration/mcp-servers.md b/docs/src/content/docs/configuration/mcp-servers.md index d8c077e8..15393e28 100644 --- a/docs/src/content/docs/configuration/mcp-servers.md +++ b/docs/src/content/docs/configuration/mcp-servers.md @@ -87,6 +87,20 @@ Claude Code files a remote server's stored OAuth token under a key derived from --- +## Inline Images from MCP Tools + +MCP tools can return image content as part of their results. When a tool returns an image (for example, a Playwright screenshot or a generated diagram), herdctl extracts the image and makes it available to the agent. In the web dashboard, these images are displayed inline in the chat conversation, providing visual context directly in the message feed. + +**Example use cases:** + +- **Screenshot tools**: A Playwright-based MCP server captures a screenshot of a web page and returns it as an image +- **Diagram generators**: A tool that creates architecture diagrams, charts, or graphs +- **Computer vision**: Tools that process images and return annotated versions + +The agent sees the image content as part of the tool result and can reference it in its response. The web dashboard automatically renders these images inline, making visual information immediately accessible. + +For more details on inline image display, see the [Web Dashboard: Inline Images](/integrations/web-dashboard/#inline-images) section. + ## Tool Naming Convention MCP tools are namespaced using the `mcp____` pattern: diff --git a/docs/src/content/docs/guides/recipes.mdx b/docs/src/content/docs/guides/recipes.mdx index 58578f22..41aad5e9 100644 --- a/docs/src/content/docs/guides/recipes.mdx +++ b/docs/src/content/docs/guides/recipes.mdx @@ -1178,6 +1178,197 @@ schedules: For integration tests, always use a temporary directory and clean up after tests to avoid polluting your workspace. +## Programmatic Schedule Management + +Manage agent schedules at runtime without rewriting `herdctl.yaml`. This is useful for building dynamic UIs or admin panels where users can create, modify, or remove schedules on the fly. + +### Enabling Schedule Mutation + +Schedule mutation must be explicitly enabled when constructing the FleetManager: + +```typescript title="dynamic-schedules.ts" +import { FleetManager } from "@herdctl/core"; + +const manager = new FleetManager({ + configPath: "./herdctl.yaml", + stateDir: "./.herdctl", + allowScheduleMutation: true, // Required for runtime schedule changes +}); + +await manager.initialize(); +await manager.start(); +``` + +### Adding a Schedule at Runtime + +Use `setAgentSchedule()` to add a new schedule or replace an existing one: + +```typescript +// Add a new interval schedule +await manager.setAgentSchedule("my-agent", "hourly-check", { + type: "interval", + interval: "1h", + prompt: "Check for updates every hour", +}); + +// Add a cron schedule +await manager.setAgentSchedule("my-agent", "daily-report", { + type: "cron", + cron: "0 9 * * *", + prompt: "Generate the daily report at 9am", +}); +``` + +The schedule starts immediately — no restart required. + +### Replacing a Schedule + +Calling `setAgentSchedule()` with an existing schedule name replaces it: + +```typescript +// Original schedule +await manager.setAgentSchedule("my-agent", "check", { + type: "interval", + interval: "5m", + prompt: "Check for new issues", +}); + +// Update the interval and prompt +await manager.setAgentSchedule("my-agent", "check", { + type: "interval", + interval: "10m", + prompt: "Check for high-priority issues only", +}); +``` + +The old schedule is removed and the new one is activated. + +### Removing a Schedule + +Use `removeAgentSchedule()` to delete a schedule by name: + +```typescript +const removed = await manager.removeAgentSchedule("my-agent", "hourly-check"); +if (removed) { + console.log("Schedule removed successfully"); +} +``` + +Returns `true` if the schedule existed and was removed, `false` if the schedule was not found. + +### When to Use Runtime Mutation + +**Use runtime schedule mutation when:** + +- Building admin dashboards where users manage agent schedules via UI +- Implementing dynamic scheduling based on external events +- Creating self-modifying agents that adjust their own schedules +- Prototyping schedules before committing them to config files + +**Use config-file schedules when:** + +- Schedules are part of the agent's core definition +- You want version control and GitOps-style deployments +- Schedules are static and don't need runtime changes +- You need schedules to survive fleet restarts without persistence logic + +Runtime schedules exist only in memory — they are not written back to `herdctl.yaml`. If you restart the fleet without re-creating them programmatically, they disappear. For persistent runtime schedules, you need to store them externally (e.g., in a database) and re-apply them on fleet startup. + +### Complete Example: Dynamic Schedule Manager + +```typescript title="schedule-manager.ts" +import { FleetManager } from "@herdctl/core"; + +class ScheduleManager { + private manager: FleetManager; + + constructor() { + this.manager = new FleetManager({ + configPath: "./herdctl.yaml", + stateDir: "./.herdctl", + allowScheduleMutation: true, + }); + } + + async start() { + await this.manager.initialize(); + await this.manager.start(); + } + + async addIntervalSchedule( + agent: string, + name: string, + interval: string, + prompt: string + ) { + await this.manager.setAgentSchedule(agent, name, { + type: "interval", + interval, + prompt, + }); + console.log(`Added interval schedule "${name}" for ${agent}`); + } + + async addCronSchedule( + agent: string, + name: string, + cron: string, + prompt: string + ) { + await this.manager.setAgentSchedule(agent, name, { + type: "cron", + cron, + prompt, + }); + console.log(`Added cron schedule "${name}" for ${agent}`); + } + + async removeSchedule(agent: string, name: string) { + const removed = await this.manager.removeAgentSchedule(agent, name); + if (removed) { + console.log(`Removed schedule "${name}" from ${agent}`); + } else { + console.warn(`Schedule "${name}" not found for ${agent}`); + } + } + + async listSchedules(agent: string) { + const agentInfo = await this.manager.getAgentInfo(agent); + return agentInfo.schedules; + } +} + +// Usage +const scheduleManager = new ScheduleManager(); +await scheduleManager.start(); + +// Add dynamic schedules +await scheduleManager.addIntervalSchedule( + "my-agent", + "check-every-30m", + "30m", + "Check for new work" +); + +await scheduleManager.addCronSchedule( + "my-agent", + "daily-summary", + "0 17 * * *", + "Generate daily summary at 5pm" +); + +// List all schedules for the agent +const schedules = await scheduleManager.listSchedules("my-agent"); +console.log(schedules); + +// Remove a schedule +await scheduleManager.removeSchedule("my-agent", "check-every-30m"); +``` + + + ## Next Steps - Check out the [FleetManager API Reference](/library-reference/fleet-manager) for complete method documentation diff --git a/docs/src/content/docs/integrations/web-dashboard.md b/docs/src/content/docs/integrations/web-dashboard.md index 5e1e9ff3..3d6cefa0 100644 --- a/docs/src/content/docs/integrations/web-dashboard.md +++ b/docs/src/content/docs/integrations/web-dashboard.md @@ -163,6 +163,27 @@ The chat feature lets you send messages to any agent directly from the dashboard Chat messages are sent via WebSocket for low-latency streaming, with the full response appearing incrementally as the agent generates it. Tool calls appear as collapsible UI components in the message feed, allowing you to inspect what the agent did and the results it received. +#### Inline Images + +Agents can emit inline images in their chat responses, and those images are displayed directly in the conversation feed: + +- **Agent-generated images**: When an agent writes an image file to its workspace and references it using markdown syntax (e.g., `![Description](/files//)`), the dashboard serves the file via a protected route and renders it inline. +- **MCP tool images**: MCP tools can return image content (for example, a Playwright screenshot tool). These images are extracted from the tool result and displayed inline in the chat. + +The dashboard serves workspace files through `GET /files/:agentName/*` with path-traversal guards, ensuring agents can only access files within their own workspace directory. This allows agents to generate diagrams, charts, screenshots, or other visual content and have them appear seamlessly in the conversation. + +**Example agent workflow:** + +```markdown +Let me create a diagram showing the architecture. + +![Architecture diagram](/files/my-agent/diagrams/architecture.png) + +As you can see in the diagram above, the system has three layers... +``` + +The image is rendered inline, making it easy to discuss visual content with your agents. + #### Session Names Sessions display an auto-generated name extracted from Claude Code's conversation summary. The display priority is: