Files
deepseek-harness/packages/api/terminal-controller/src/agent.ts
T
Yichen Jiang 9eb4144222 refactor(terminal-controller): publish AgentTerminal as an interface
The public AgentTerminal class took the internal BrowserTerminal as its
constructor parameter, so the Cordis API catalog listed BrowserTerminal
and all of its members. AgentTerminal is now a type-only interface and
the controller constructs a package-private AgentTerminalHandle; the
catalog no longer names BrowserTerminal. BrowserTerminal.onStateChange
becomes setStateListener, because a later call replaces the listener.
2026-11-04 15:41:10 +08:00

140 lines
5.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/** Agent access to Session terminals: control checks, typed failures and the agent's terminal handle. */
import type { SandboxExecutionPolicy } from '@deepseek-ai/dsh-sandbox'
import type { SubprocessTerminalForeground, SubprocessTerminalSignal } from '@deepseek-ai/dsh-subprocess'
import type { BrowserTerminal } from './terminal.ts'
import type { TerminalScreenMark, TerminalScreenPage, TerminalScreenRequest, WebTerminalId, WebTerminalInfo } from './types.ts'
/** Why an agent operation on a terminal was refused. */
export type AgentTerminalErrorCode = 'user-control' | 'not-running' | 'unavailable'
/** A refused agent terminal operation; the terminal and its other attachments are unchanged. */
export class AgentTerminalError extends Error {
/**
* @param code - machine-routable refusal reason.
* @param message - English description for logs and model-facing errors.
*/
constructor(readonly code: AgentTerminalErrorCode, message: string) {
super(message)
this.name = 'AgentTerminalError'
}
}
/** Launch parameters for an agent terminal; the caller resolves policy and directory. */
export interface AgentTerminalRequest {
/** Tab title of 1–120 characters; surrounding whitespace is removed. */
readonly title: string
/** Absolute initial directory; absent uses the Session working directory. */
readonly cwd?: string
readonly cols: number
readonly rows: number
/** Policy resolved from the Session sandbox policy; confined modes wrap the shell. */
readonly policy: SandboxExecutionPolicy
/** Executable names tried in order among discovered shells before the environment default. */
readonly preferredShells: readonly string[]
/**
* Variables added to the shell environment; the controller's `DSH_SESSION_ID`, and `HISTFILE` in confined
* POSIX shells, take precedence.
*/
readonly env?: Readonly<Record<string, string>>
}
/**
* The agent's capability over one terminal it opened. Input, signals and closing require Agent control;
* reads continue after the person takes over.
*/
export interface AgentTerminal {
/** Host identity of the sidebar terminal. */
readonly id: WebTerminalId
/** Top-level process id; an unconfined or exec-confined shell leads its own process group with it. */
readonly pid: number
/** Current metadata, including control and process state. */
readonly info: WebTerminalInfo
/** Advances after applied output and published state changes. */
readonly revision: number
/**
* Wait until output or state changes after a known revision.
* @param after - revision the caller has observed.
* @param signal - wait cancellation.
* @returns the current revision.
*/
changed(after: number, signal: AbortSignal): Promise<number>
/**
* Write input in order with the person's input and output.
* @param data - text delivered without implicit newline conversion.
* @returns after the provider accepts the input.
*/
write(data: string): Promise<void>
/**
* Wait until output received before the call is applied to the screen.
* @returns after queued output and input settle.
*/
drain(): Promise<void>
/**
* Render a newest-relative page of the screen as applied so far.
* @param request - page position and byte bound.
* @returns rendered text.
* @throws AgentTerminalError `unavailable` after the terminal is closed.
*/
read(request: TerminalScreenRequest): TerminalScreenPage
/**
* Mark the start of the cursor's logical line on the screen as applied so far, so later output can be rendered from it.
* @returns the screen mark.
* @throws AgentTerminalError `unavailable` after the terminal is closed.
*/
mark(): TerminalScreenMark
/**
* Inspect the foreground process group.
* @returns the group and its input-wait fact, or undefined when unknown.
*/
inspectForeground(): Promise<SubprocessTerminalForeground | undefined>
/**
* Signal the foreground process group.
* @param signal - permitted terminal signal.
* @returns the process group that received it.
*/
signal(signal: SubprocessTerminalSignal): Promise<number>
/**
* Terminate the terminal and remove its tab while the agent controls it.
* Control is checked once when the call starts; a takeover during cleanup does not stop the close.
* @returns after process cleanup.
* @throws AgentTerminalError after the person takes over; the terminal then stays open for the person.
*/
close(): Promise<void>
}
/** The controller's {@link AgentTerminal} over one committed Session terminal; the package does not export it. */
export class AgentTerminalHandle implements AgentTerminal {
/**
* @param terminal - committed Session terminal.
* @param closeTerminal - closes the identity through its Session owner.
*/
constructor(private readonly terminal: BrowserTerminal, private readonly closeTerminal: () => Promise<void>) {}
get id(): WebTerminalId { return this.terminal.info.id }
get pid(): number { return this.terminal.pid }
get info(): WebTerminalInfo { return this.terminal.info }
get revision(): number { return this.terminal.revision }
changed(after: number, signal: AbortSignal): Promise<number> { return this.terminal.changed(after, signal) }
write(data: string): Promise<void> { return this.terminal.agentWrite(data) }
drain(): Promise<void> { return this.terminal.drain() }
read(request: TerminalScreenRequest): TerminalScreenPage { return this.terminal.readScreen(request) }
mark(): TerminalScreenMark { return this.terminal.markScreen() }
inspectForeground(): Promise<SubprocessTerminalForeground | undefined> { return this.terminal.inspectForeground() }
signal(signal: SubprocessTerminalSignal): Promise<number> { return this.terminal.agentSignal(signal) }
async close(): Promise<void> {
if (this.terminal.info.control !== 'agent') throw new AgentTerminalError('user-control', 'The user has taken over this terminal')
await this.closeTerminal()
}
}