Skip to content

feat: add experimental Monitor tools for event-driven watchers - #3132

Open
cgkoreyoshi wants to merge 5 commits into
MoonshotAI:mainfrom
cgkoreyoshi:feat/monitor-watchers
Open

feat: add experimental Monitor tools for event-driven watchers#3132
cgkoreyoshi wants to merge 5 commits into
MoonshotAI:mainfrom
cgkoreyoshi:feat/monitor-watchers

Conversation

@cgkoreyoshi

Copy link
Copy Markdown

Related Issue

No linked issue — the problem is explained in the next section.

Problem

When the agent starts long-running work (builds, test suites, dev servers), it can only wait by polling: repeated TaskOutput/TaskList calls or sleep loops. Every poll burns tokens, fills the context window with "still running" noise, and adds latency between the event and the agent's reaction. Kimi Code already pushes background-task terminal notifications back into the agent loop, but there is no way to watch for output patterns (e.g. a dev server's "listening on port 3000"), arbitrary external commands, or file changes.

What changed

Adds an experimental Monitor capability to both engines, gated behind KIMI_CODE_EXPERIMENTAL_MONITOR (default off):

  • Tool surface (both engines): MonitorCreate / MonitorList / MonitorCancel. Three watcher types:
    • task_output — regex-match a background task's stdout/stderr line by line, firing without waiting for the task to finish (output produced before registration is replayed through the matcher);
    • command — run and watch any shell command (e.g. tail -f app.log), firing on the first pattern match (then killing the process), on command exit, or on timeout;
    • file — watch a file/directory/glob for created/modified events, with an optional regex filter on the changed path.
      All monitors are one-shot, carry a timeout (default 1h, max 24h), are capped at 20 active per agent, and resume as lost after a restart (never silently re-attached).
  • v1 (packages/agent-core): MonitorManager on the main agent; notifications reuse renderNotificationXml and are pushed via turn.steer with a new MonitorOrigin kind.
  • v2 (packages/agent-core-v2): AgentMonitorService (Agent scope, eager activation); notifications mirror the task-notification path via a mergeable MonitorNotificationStepRequest; delivery state is replayable().undoable(); persistence via IAtomicDocumentStore; telemetry events registered.
  • Cross-package: monitor branch in the origin zod unions (protocol, kap-server), transcript TurnOrigin + contract schema, kap-server coreEventMap, TUI replay/export rendering. MonitorCreate/MonitorCancel are denied in plan mode in both engines.
  • Fixes found through real-CLI verification (each with regression tests):
    • v2 service made eager — an OnDemand service contributes replayable state after the dispatcher's restore window and crashes agent creation;
    • toInputJsonSchema (both engines) re-asserts type: "object" on anyOf/oneOf roots — providers reject union parameter schemas without it (400);
    • task_output watchers replay the task's existing output at subscription time so patterns emitted before registration are not missed;
    • file watch paths are canonicalized through symlinks (macOS /tmp/private/tmp reported events on the symlink node);
    • model-facing input ergonomics: the timeout input is named timeout (matching the Bash tool's convention) and file monitors accept an optional pattern.
  • Domain documentation: packages/agent-core-v2/docs/monitor.md (Chinese), also linked from the package's AGENTS.md.

Tests: new monitor suites in both engines (unit + tool surface + a real-agent scripted-model E2E), schema-conversion pins, and a dispatcher-phase regression test; the node-sdk parity suite passes unmodified (flag default off keeps both tool lists clean). Verified end-to-end with the built CLI: a three-watcher demo (background task + task_output, tail -f command, file creation) delivers all three notifications mid-turn.

Checklist

  • I have read the CONTRIBUTING document.
  • I have linked a related issue, or explained the problem above.
  • I have added tests that prove my feature works.
  • Ran gen-changesets skill, or this PR needs no changeset.
  • Ran gen-docs skill, or this PR needs no doc update. (No user-doc update: the feature is experimental and flag-gated, default off; the changeset documents how to enable it.)

皓陈 added 5 commits August 20, 2026 16:14
Add MonitorCreate/MonitorList/MonitorCancel tools to both engines so the
agent can register one-shot listeners on background task output, shell
commands, and file changes, and get interrupted when they trigger
instead of polling. Gated behind the monitor experimental flag
(KIMI_CODE_EXPERIMENTAL_MONITOR, default off).

Also re-assert type:"object" at the root of union tool parameter
schemas; model providers reject parameter schemas whose root lacks it.
task_output monitors now replay the task's existing output through the
matcher at subscription time, so a pattern emitted between task start
and monitor registration is not missed.

file monitors canonicalize watch paths through symlinks (nearest
existing ancestor realpath), fixing macOS /tmp → /private/tmp watches
where chokidar reports the change on the symlink node and neither the
exact-path comparison nor glob filtering would match.
Models are primed by the Bash tool's timeout parameter (seconds) in the
same toolset and kept calling MonitorCreate with timeout=..., which the
closed-object union schema rejected. Rename the field to match the
established convention, and clarify the trigger-vs-input wording in the
tool description.
Models naturally generalize the pattern field from the other two
monitor types and called type:file with path+pattern, which the
closed-object union schema rejected. Accept it: a JavaScript regex
matched against the changed file path, composing with the path/glob
watch root.
@changeset-bot

changeset-bot Bot commented Aug 20, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 6c54b2d

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@moonshot-ai/kimi-code Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6c54b2dbb6

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".


private async setupFileMonitor(managed: ManagedMonitor, spec: FileMonitorSpec): Promise<void> {
const record = managed.record;
const absolute = canonicalizeForWatch(resolve(this.session.cwd, spec.path));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Bind v2 file monitors to the active runtime

When an agent is bound to a container or remote runtime, this resolves and canonicalizes the path on the server host and later watches it through the App-scoped IHostFsWatchService, rather than through runtime.acquire(['watch']). Changes made by Bash or other tools inside the active runtime therefore never fire the monitor, and a coincidentally matching host path may be watched instead; resolve the path with the runtime workspace mapping and retain a lease on lease.runtime.watch for the monitor lifetime.

Useful? React with 👍 / 👎.

Comment on lines +405 to +407
const env = lease.runtime.environment;
const cwd = env.osKind === 'Windows' ? windowsPathToPosixPath(this.session.cwd) : this.session.cwd;
const shellCommand = `cd ${shellQuote(cwd)} && ${command}`;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Map command monitor cwd into the active runtime

For a non-local runtime whose workspace roots differ from the host session path, this constructs cd from this.session.cwd directly. The Bash tool maps roots through RuntimeWorkspaceView before spawning, but command monitors skip that mapping, so the remote shell exits because the directory does not exist and the monitor reports a misleading command-exit notification. Resolve the cwd through the acquired runtime's workspace/path view before building the shell command.

Useful? React with 👍 / 👎.

live: LiveMonitor,
spec: Extract<MonitorCreateSpec, { type: 'file' }>,
): Promise<void> {
const absolute = canonicalizeForWatch(resolve(this.agent.config.cwd, spec.path));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Bind v1 file monitors to the Kaos environment

When Agent.kaos is an SSH or container implementation, this uses Node's local resolve/realpathSync and chokidar against the CLI host even though the watched workspace and commands live in Kaos. A file created by a remote background task will therefore never trigger this monitor, while an unrelated local path can trigger it; the watcher needs an execution-environment-aware implementation or must explicitly reject non-local Kaos environments.

Useful? React with 👍 / 👎.

Comment on lines +317 to +320
let proc: IHostProcess;
try {
proc = lease.track(await this.spawnMonitorCommand(lease, spec.command));
} catch (error) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Close monitored commands' stdin after spawning

When a monitored command reads stdin, such as cat, read, or a CLI that conditionally consumes piped input, the pipe remains open forever because this path never calls proc.stdin.end(). Such a command cannot reach its expected exit and instead remains active until the monitor timeout; close stdin immediately after spawning, as the Bash tool and the v1 monitor implementation do.

Useful? React with 👍 / 👎.

Comment on lines +294 to +298
void this.tasks.readOutput(spec.taskId).then(
(backlog) => {
this.feedChunk(managed, backlog);
},
() => {},

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Replay v2 task backlog before terminal teardown

When a running task already contains a matching line and terminates while this asynchronous readOutput is still pending, onTaskTerminated changes the monitor to ended; the later feedChunk then ignores the backlog because the status is no longer active. This loses the explicitly promised pre-registration match and sends no notification, so backlog replay must be coordinated with terminal handling rather than launched fire-and-forget.

Useful? React with 👍 / 👎.

Comment on lines +488 to +492
void this.agent.background.readOutput(spec.taskId).then(
(backlog) => {
matcher.feed(backlog);
},
() => {},

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Replay v1 task backlog before terminal teardown

When the target has already emitted a matching line and reaches terminal state while this asynchronous backlog read is pending, watchTargetTerminal can mark the monitor ended first; the matcher callback subsequently calls fire, which refuses the now-terminal monitor. The advertised replay of output produced before registration is therefore lost for short-lived tasks, so creation must finish or otherwise synchronize backlog replay before terminal teardown wins.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant