feat: add experimental Monitor tools for event-driven watchers - #3132
feat: add experimental Monitor tools for event-driven watchers#3132cgkoreyoshi wants to merge 5 commits into
Conversation
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 detectedLatest commit: 6c54b2d The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
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 |
There was a problem hiding this comment.
💡 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)); |
There was a problem hiding this comment.
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 👍 / 👎.
| const env = lease.runtime.environment; | ||
| const cwd = env.osKind === 'Windows' ? windowsPathToPosixPath(this.session.cwd) : this.session.cwd; | ||
| const shellCommand = `cd ${shellQuote(cwd)} && ${command}`; |
There was a problem hiding this comment.
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)); |
There was a problem hiding this comment.
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 👍 / 👎.
| let proc: IHostProcess; | ||
| try { | ||
| proc = lease.track(await this.spawnMonitorCommand(lease, spec.command)); | ||
| } catch (error) { |
There was a problem hiding this comment.
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 👍 / 👎.
| void this.tasks.readOutput(spec.taskId).then( | ||
| (backlog) => { | ||
| this.feedChunk(managed, backlog); | ||
| }, | ||
| () => {}, |
There was a problem hiding this comment.
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 👍 / 👎.
| void this.agent.background.readOutput(spec.taskId).then( | ||
| (backlog) => { | ||
| matcher.feed(backlog); | ||
| }, | ||
| () => {}, |
There was a problem hiding this comment.
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 👍 / 👎.
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/TaskListcalls orsleeploops. 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):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 forcreated/modifiedevents, 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 aslostafter a restart (never silently re-attached).packages/agent-core):MonitorManageron the main agent; notifications reuserenderNotificationXmland are pushed viaturn.steerwith a newMonitorOriginkind.packages/agent-core-v2):AgentMonitorService(Agent scope, eager activation); notifications mirror the task-notification path via a mergeableMonitorNotificationStepRequest; delivery state isreplayable().undoable(); persistence viaIAtomicDocumentStore; telemetry events registered.monitorbranch in the origin zod unions (protocol,kap-server),transcriptTurnOrigin+ contract schema, kap-servercoreEventMap, TUI replay/export rendering.MonitorCreate/MonitorCancelare denied in plan mode in both engines.OnDemandservice contributes replayable state after the dispatcher's restore window and crashes agent creation;toInputJsonSchema(both engines) re-assertstype: "object"onanyOf/oneOfroots — providers reject union parameter schemas without it (400);task_outputwatchers replay the task's existing output at subscription time so patterns emitted before registration are not missed;/tmp→/private/tmpreported events on the symlink node);timeout(matching the Bash tool's convention) andfilemonitors accept an optionalpattern.packages/agent-core-v2/docs/monitor.md(Chinese), also linked from the package'sAGENTS.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 -fcommand, file creation) delivers all three notifications mid-turn.Checklist
gen-changesetsskill, or this PR needs no changeset.gen-docsskill, 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.)