Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions tools/agent-monitor/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
node_modules/
dist/
164 changes: 164 additions & 0 deletions tools/agent-monitor/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
# Agent monitor

Independent, read-only terminal viewer for **one explicitly selected local Copilot
CLI session**. It does not change the built-in `/tasks` interface, load the Copilot
SDK, resume the session, install hooks, call a model, or write to the source log.

## Screenshots

Captured from a fictional demo session (synthetic events, no real transcript).

![Graph view: the spawn tree with launch modes and recorded wait durations](docs/graph.png)

![Graph view: an agent blocked on the sync agent it launched](docs/graph-waiting.png)

![Timeline view: swimlanes with waits, a later turn and the now cursor](docs/timeline.png)

## Run on Windows

Requires Node.js 22 or later. In PowerShell:

```powershell
Set-Location "<checkout>\tools\agent-monitor"
npm ci
npm run build
$session = Join-Path $env:USERPROFILE ".copilot\session-state\<session-id>"
node .\dist\cli.js --session $session
```

Run this in a **separate interactive terminal tab** alongside Copilot. The viewer
replays the selected directory's `events.jsonl`, then follows newly appended
records. The directory can also belong to a historical session. There is no
automatic scan of other sessions.

For development, use `npm start -- --session "<absolute-session-directory>"`.
Use `node .\dist\cli.js --help` for command-line help. Add `--view timeline` to
open on the Timeline. Add `--theme <mode>` to
match the Copilot CLI `/theme` color mode: `default`, `light`, `dim`,
`high-contrast`, or `colorblind` (Primer semantic colors; needs a truecolor
terminal such as Windows Terminal). Minimum size: 80×24.

## Open it from Copilot CLI (experimental)

[`extension/`](extension/extension.mjs) is a
[Copilot CLI extension](https://docs.github.com/en/copilot/tutorials/create-an-extension)
that adds an `/agent-monitor` command to the session you are working in:

- `/agent-monitor` prints the session's agent tree in the conversation and opens
the Graph view beside the CLI.
- `/agent-monitor timeline` does the same with the Timeline view.
- `/agent-monitor text` only prints the tree.

The interactive view opens as a Windows Terminal split pane (or a tmux split);
in other terminals the command prints the line to run in another tab.
Extensions are an experimental Copilot CLI feature. After `npm run build`, link
this folder into your Copilot home once:

```powershell
New-Item -ItemType Junction -Path "$HOME\.copilot\extensions\agent-monitor" -Target "$PWD\extension"
```

Then start Copilot CLI with `--experimental` (or run `/experimental on`) and use
`/agent-monitor`. Remove the junction to uninstall. Copilot CLI has no extension
API for adding tabs to its own tab bar, so the views open beside it.

## Views and keys

The layout follows Copilot CLI's own interface: a tab bar, blue query chips, a
`·`-separated key-hint footer, and a bordered detail dialog. Two views share
one selection:

- **Graph** (default): a spawn tree. The main session sits on top; agents sit
under the agent or session that launched them, and a reused agent's later
turns sit side by side under its first turn. Edge labels show `bg` / `sync`
and any recorded wait with its duration (yellow while waiting, dim once
finished), naming the waiter when it is not the edge's parent. Cards narrow
to fit a wide fan-out; beyond that the graph scrolls horizontally to keep
the selection visible.
- **Timeline**: one swimlane per execution, with the main session's model
turns on the first lane. Bars are colored by outcome, `╍` marks time spent
waiting, `▾` connects a waiting lane to the execution it waited for, and the
`now` cursor follows the session. The default window covers the period in
which agents ran.

The bottom dialog shows what the selected execution was asked (its task
prompt), its final message or error (the latest message while it runs), and its
recorded relationships. Tool calls stay hidden unless you press `t`. Long text
is loaded from the selected log on demand rather than copied to a new database.

| Key | Action |
| --- | --- |
| Up/Down or `k`/`j` | Select the previous/next execution |
| `g` / `G` | Select the first/last execution |
| Left/Right or `h`/`l` | Graph: previous/next sibling. Timeline: pan earlier/later |
| `+` / `-` | Timeline: zoom in/out (beyond the agent window up to the whole session) |
| Tab or `v` | Switch between Graph and Timeline |
| `f` | Cycle all, active, finished; ancestors remain as dimmed context |
| `t` | Show tool calls in the detail dialog instead of relationships (below 100 columns: instead of the message) |
| Page Up / Page Down | Scroll the selected result |
| `q` or Escape | Quit and restore the previous terminal screen |

`done` means an invocation has a recorded `subagent.completed` event or an
accepted agent-attributed `session.task_complete`. `cancelled` means
`subagent.completed` recorded `cancelled: true` (for example, a superseded run);
it is not a failure. `replied` means the agent's latest model turn ended with a
final answer, not progress commentary, but lifecycle completion was not
recorded. Lifecycle events name an agent's original launch call; they settle
the agent's open execution and never overwrite an earlier recorded outcome.
Follow-up executions on a reused agent remain separate from its earlier work.
If a follow-up arrives while the earlier execution has no recorded outcome, the
earlier one becomes `unknown` instead of staying `running`.
The finished filter includes replied executions without claiming the agent was
terminated. Running/unknown states are **last observed**, not a process-health
check; silence or end-of-file does not prove that an agent is dead or finished.

## Relationships and compatibility

Compatibility was verified against Copilot CLI **1.0.104** and **1.0.105**, using
a local parallel/nested-agent trace, a same-agent follow-up, and a replay of local
session logs. The on-disk event format
is version-sensitive, not a supported stable public attachment API. Other CLI
versions display a compatibility warning; malformed or unsupported relevant
records produce explicit diagnostics rather than fabricated results.

- Invocation parents use verified launch-call ownership or
`subagent.started.data.parentId`. Envelope `parentId` is the event chain and
is never interpreted as an agent edge.
- Temporary waits use synchronous invocation or explicit `read_agent`
`wait: true` data. A nonblocking status read is not a dependency.
- The verified format has no general prerequisite/`depends_on` DAG. Its absence
is displayed explicitly. No graph edges are inferred from timing or prose.
- Background launch acknowledgement is not task completion.
- Missing outputs stay unavailable. Original text may already have been
truncated by Copilot; the viewer cannot reconstruct it.
- Ephemeral events and unrecorded cancellation/liveness signals cannot be
recovered from this file. Unknown agent-event types are diagnosed, not
interpreted as an outcome.
- Detected source replacement/truncation rebuilds the view. A trailing record
without a newline remains buffered until it is complete. Reading stops on a source
error, with a visible diagnostic and a nonzero exit status on quit; the clock
then stays at the last recorded event.
- While following a live log, open executions extend to the current time. A
recorded `session.shutdown` switches the header to `ended` and stops the clock;
a later `session.resume` makes the session live again.
- Below 100 columns the detail dialog uses one column and omits relationships.

Text from the log is rendered without terminal control sequences. Session
content stays local: do not copy private transcripts into this repository.
This private package is a standalone local tool, not a modified distribution
of Copilot CLI or a claim that upstream accepts core CLI changes here.

## Validate

```powershell
npm run typecheck
npm test
npm run build
```

Tests use synthetic records and temporary files, covering replay/tailing,
UTF-8 boundaries, source reset, agent/result attribution, lifecycle routing and
cancellation, session resume, recorded wait history, reused agents and later
turns, graph layout and scrolling, the Graph/Timeline views, filtering,
hidden-by-default tool calls, narrow terminals, result scrolling, and the
text summary printed by the extension.
Binary file added tools/agent-monitor/docs/graph-waiting.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added tools/agent-monitor/docs/graph.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added tools/agent-monitor/docs/timeline.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
56 changes: 56 additions & 0 deletions tools/agent-monitor/extension/extension.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// Copilot CLI extension (experimental): `/agent-monitor` prints the current session's agent tree in the conversation and
// opens the interactive Graph or Timeline view beside the CLI. Run `npm run build` in tools/agent-monitor first.
import {spawn} from 'node:child_process';
import {realpathSync} from 'node:fs';
import {homedir} from 'node:os';
import {basename, dirname, join} from 'node:path';
import {fileURLToPath, pathToFileURL} from 'node:url';
import {joinSession} from '@github/copilot-sdk/extension';

// Resolve the package through a linked extension directory, so the build next to this file is used.
const root = dirname(dirname(realpathSync(fileURLToPath(import.meta.url))));
const cli = join(root, 'dist', 'cli.js');
const {summarizeSession} = await import(pathToFileURL(join(root, 'dist', 'summary.js')).href);
const node = /^node(\.exe)?$/i.test(basename(process.execPath)) ? process.execPath : 'node';
const CHOICES = ['graph', 'timeline', 'text'];

const session = await joinSession({
commands: [{
name: 'agent-monitor',
description: 'Show this session\'s agents here and open the Graph or Timeline view beside the CLI: /agent-monitor [graph|timeline|text]',
handler: async ctx => {
const choice = ctx.args.trim().toLowerCase() || 'graph';
if (!CHOICES.includes(choice)) {
await session.log('Usage: /agent-monitor [graph|timeline|text]', {level: 'warning', ephemeral: true});
return;
}
const sessionDir = session.workspacePath ?? join(process.env.COPILOT_HOME ?? join(homedir(), '.copilot'), 'session-state', ctx.sessionId);
try {
await session.log(await summarizeSession(sessionDir), {ephemeral: true});
} catch (error) {
await session.log(`agent-monitor: ${error instanceof Error ? error.message : String(error)}`, {level: 'error', ephemeral: true});
return;
}
if (choice !== 'text') openBeside(sessionDir, choice);
},
}],
});

/** Opens the viewer in a split pane where the terminal supports one; otherwise prints the command to run. */
function openBeside(sessionDir, view) {
const args = [cli, '--session', sessionDir, '--view', view];
const manual = () => session.log(
`Run this in another terminal tab for the interactive view:\n${[node, ...args].map(arg => JSON.stringify(arg)).join(' ')}`,
{ephemeral: true},
);
const pane = process.env.WT_SESSION
? ['wt.exe', ['-w', '0', 'split-pane', '--vertical', '--title', 'Agent monitor', '--', node, ...args]]
: process.env.TMUX ? ['tmux', ['split-window', '-h', node, ...args]] : undefined;
if (!pane) {
void manual();
return;
}
const child = spawn(pane[0], pane[1], {detached: true, stdio: 'ignore', windowsHide: true});
child.on('error', () => void manual());
child.unref();
}
Loading