Skip to content
Merged
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
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ Use [OpenAI Codex](https://github.com/openai/codex) from [Agent Client Protocol]
- Text prompts, embedded context, images, resource links, and additional workspace directories.
- Shell command, file change, [permission request](docs/permission-extension.md), MCP tool call, terminal output, reasoning, plan, web search, image generation, image view, token usage, and review events.
- [Native ACP subagent sessions](docs/subagent-sessions.md) (after capability negotiation) with separate child histories and root-routed permissions; a legacy tool-call fallback otherwise.
- [Background terminal tasks](docs/async-tasks.md) in AIR, with task status and targeted stop support after capability negotiation.
- Session-scoped long-running goals through the provider-neutral [goal extension](docs/goal-extension.md).
- A per-turn [agent file-change report](docs/agent-file-change-report.md) after capability negotiation.
- Client-provided MCP servers over command-based stdio config and HTTP transport.
Expand Down Expand Up @@ -82,6 +83,12 @@ Subagent sessions follow the draft [ACP subagent RFD](https://github.com/agentcl

See [docs/subagent-sessions.md](docs/subagent-sessions.md) for the negotiation, lifecycle events, `session/load` reconstruction, and legacy fallback details.

### Background terminal tasks

Codex can keep a shell command running after a turn continues. AIR clients can show this work in the Async Tasks panel and stop one command.

See [docs/async-tasks.md](docs/async-tasks.md) for the capability, lifecycle events, and stop request.

## License

By contributing, you agree that your contributions will be licensed under the Apache 2.0 License.
42 changes: 42 additions & 0 deletions docs/async-tasks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Background terminal tasks

Codex app-server owns shell commands that continue after their initial tool call. The adapter exposes these commands through the AIR async task extension.

## Negotiation

The client adds `asyncTasks` to `_meta.jetbrains.air.capabilities`. The adapter advertises the same capability in its `initialize` response.

The adapter emits no async task updates when the client does not advertise this capability.

## Lifecycle

The adapter uses `thread/backgroundTerminals/list` as the source of active processes. It maps each active process to `async_task_spawned`.

Before the spawn update, the adapter marks the command with `_meta.jetbrains.air.asyncTasks.backgrounded`. AIR can then keep the command card active without duplicating its output.

For a root command, the command item ID is both the async task ID and the related tool call ID. A child command prefixes its task ID with the child thread ID. The prefix keeps task IDs distinct across native subagent sessions. The related tool call ID remains the command item ID.

The adapter publishes a child command on its native subagent session. The app-server process ID remains an internal control handle.

The existing command card owns the command output. Therefore, a background terminal task sets `showInTranscript` to `false`.

When the command ends, the adapter emits `async_task_state_update` with `completed` or `failed`.

The active-terminal list repairs a lost completion event. The adapter reports `stopped` when an announced terminal disappears from that list.

Session loading restores root and child tasks after it replays their command history. A provider restart stops old tasks and moves task control to the new app-server client.

If the app-server exits, the adapter reports each unfinished task as `failed`.

## Stop request

The client sends `_session/async_task/stop` with the ACP session ID and async task ID:

```json
{
"sessionId": "thread-id",
"asyncTaskId": "command-item-id"
}
```

The adapter resolves the app-server process ID and calls `thread/backgroundTerminals/terminate`. It returns `{ "stopped": true }` after app-server accepts the termination.
2 changes: 1 addition & 1 deletion src/ACPSessionConnection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import * as acp from "@agentclientprotocol/sdk";
import {
type AcpSessionUpdate,
asSdkSessionNotification,
} from "./subagents/AcpSubagents";
} from "./AcpSessionExtensions";

export type AcpClientConnection = Pick<acp.AgentContext, "notify" | "request">;

Expand Down
8 changes: 7 additions & 1 deletion src/AcpExtensions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ import {
LEGACY_GOAL_CONTROL_METHOD,
type GoalControlRequest,
} from "./GoalExtension";
import {
ASYNC_TASK_STOP_METHOD,
type AsyncTaskStopExtRequest,
} from "./async-tasks/AsyncTaskExtension";

export {
AUTH_STATUS_META_KEY,
Expand Down Expand Up @@ -74,14 +78,16 @@ export type ExtMethodRequest =
| LegacySetSessionModelExtRequest
| SessionSteeringExtRequest
| GoalControlExtRequest
| AsyncTaskStopExtRequest

export function isExtMethodRequest(request: { method: string, params: Record<string, unknown> }): request is ExtMethodRequest {
return request.method === "authentication/status"
|| request.method === "authentication/logout"
|| request.method === LEGACY_SET_SESSION_MODEL_METHOD
|| request.method === GOAL_CONTROL_METHOD
|| request.method === LEGACY_GOAL_CONTROL_METHOD
|| request.method === SESSION_STEERING_METHOD;
|| request.method === SESSION_STEERING_METHOD
|| request.method === ASYNC_TASK_STOP_METHOD;
}

/**
Expand Down
21 changes: 21 additions & 0 deletions src/AcpSessionExtensions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import type {SessionNotification} from "@agentclientprotocol/sdk";
import type {AsyncTaskUpdate} from "./async-tasks/AcpAsyncTasks";
import type {SubagentSpawnedUpdate, SubagentStateUpdate} from "./subagents/AcpSubagents";

/** Session updates that are not available in the published ACP SDK yet. */
export type AcpSessionUpdate =
| SessionNotification["update"]
| SubagentSpawnedUpdate
| SubagentStateUpdate
| AsyncTaskUpdate;

type AcpSessionNotification = Omit<SessionNotification, "update"> & {
update: AcpSessionUpdate;
};

/** The only cast needed until the ACP SDK publishes the extension updates. */
export function asSdkSessionNotification(
notification: AcpSessionNotification,
): SessionNotification {
return notification as SessionNotification;
}
2 changes: 2 additions & 0 deletions src/AirExtension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ export const AIR_EXTENSION_CAPABILITIES_KEY = "capabilities";
export const AIR_SESSION_FAILURE_KEY = "sessionFailure";
export const AIR_AGENT_FILE_CHANGE_REPORT_KEY = "agentFileChangeReport";
export const AIR_NATIVE_SUBAGENT_SESSIONS_KEY = "nativeSubagentSessions";
export const AIR_ASYNC_TASKS_KEY = "asyncTasks";
export const AIR_ASYNC_TASKS_BACKGROUNDED_KEY = "backgrounded";
export const AIR_AGENT_FILE_CHANGE_REPORT_REQUEST_KEY = "agentFileChangeReportRequest";
export const AIR_EXTENSION_VERSION = 1;

Expand Down
Loading