Compare commits

...

1 Commits

Author SHA1 Message Date
mark.tkachenko
3dba368bc4 refactor: move the v1 steering and async task stop requests out of acp-agent.ts 2026-11-10 18:39:40 +01:00
4 changed files with 123 additions and 107 deletions

View File

@@ -32,8 +32,8 @@ import {
ClaudeAcpAgent,
computeSessionFingerprint,
runPromptWithCancellation,
type SteerRequest,
} from "../../v1/acp-agent.js";
import type { SteerRequest } from "../../v1/steering.js";
import { buildConfigOptions, discoverCustomAgents } from "../../session-config-options.js";
import { createFastModeConfigOption } from "../../session-fast-mode.js";
import {

View File

@@ -147,6 +147,20 @@ import {
clientSupportsAsyncTasks,
} from "../async-tasks.js";
import { SessionBackgroundWork } from "./background-work.js";
import {
ASYNC_TASK_STOP_METHOD,
parseAsyncTaskStopRequest,
type AsyncTaskStopRequest,
type AsyncTaskStopResponse,
} from "./async-task-stop.js";
import {
STEER_METHOD,
STEER_PRIORITY_LATER,
STEER_PRIORITY_NOW,
parseSteerRequest,
type SteerRequest,
type SteerResponse,
} from "./steering.js";
import {
AUTH_STATUS_UPDATE_METHOD,
type AuthStatus,
@@ -562,112 +576,6 @@ const TURN_NO_RESULT_MESSAGE =
"The turn ended without a result: the agent went idle while this prompt was still in flight " +
"(e.g. the model stream dropped mid-turn). Any partial output may be incomplete; please retry.";
/** Custom (extension) request method a client uses to steer the turn that is
* currently running: the message is injected into the in-flight turn rather
* than queued as a separate `session/prompt`. Named `_session/steering` per the
* agreed ACP steering wire protocol; advertised to clients via the top-level
* `InitializeResponse._meta.steering.supported`. */
const STEER_METHOD = "_session/steering";
/** Stops one Claude background task without cancelling the parent prompt turn. */
const ASYNC_TASK_STOP_METHOD = "_session/async_task/stop";
type AsyncTaskStopRequest = {
sessionId: string;
asyncTaskId: string;
};
type AsyncTaskStopResponse = {
stopped: boolean;
};
function parseAsyncTaskStopRequest(value: unknown): AsyncTaskStopRequest {
if (!value || typeof value !== "object" || Array.isArray(value)) {
throw RequestError.invalidParams(undefined, "async task stop params must be an object");
}
const params = value as Record<string, unknown>;
const sessionId = typeof params.sessionId === "string" ? params.sessionId.trim() : "";
const asyncTaskId = typeof params.asyncTaskId === "string" ? params.asyncTaskId.trim() : "";
if (!sessionId) {
throw RequestError.invalidParams(
undefined,
"async task stop params require a non-empty sessionId",
);
}
if (!asyncTaskId) {
throw RequestError.invalidParams(
undefined,
"async task stop params require a non-empty asyncTaskId",
);
}
return { sessionId, asyncTaskId };
}
/** How urgently the SDK delivers a steered message relative to the running
* turn — an internal Claude implementation detail, not part of the wire
* contract. `now` pre-empts the current generation, while `later` waits for a
* pending permission/elicitation callback to settle instead of cancelling its
* ACP request and hiding the client's user-input card (IJAI-1191). */
const STEER_PRIORITY_NOW = "now" as const;
const STEER_PRIORITY_LATER = "later" as const;
/** Request-level steering options. `promptRequired` is opt-in so existing Hosts
* keep the established idle fallback behavior. */
type SteerMeta = {
[key: string]: unknown;
steering?: {
idleBehavior?: "promptRequired";
};
};
/** Params of a {@link STEER_METHOD} request. Shaped like the relevant subset of
* a `PromptRequest` so the same `promptToClaude` conversion applies. Delivery
* priority is deliberately NOT exposed here — it's an internal detail the agent
* chooses (see {@link STEER_PRIORITY}). */
export type SteerRequest = {
sessionId: string;
prompt: PromptRequest["prompt"];
_meta?: SteerMeta | null;
};
/** Result of a {@link STEER_METHOD} request. The legacy `startedNewTurn` result
* remains the default idle behavior; `promptRequired` is returned only when the
* Host explicitly opts into the host-owned fallback in request `_meta`. */
export type SteerResponse =
| { outcome: "injected" }
| { outcome: "startedNewTurn" }
| { outcome: "promptRequired"; reason: "noRunningTurn" };
/** Validate raw JSON-RPC params into a {@link SteerRequest}. Kept minimal — the
* content blocks are handed to `promptToClaude`, which tolerates unknown block
* types — but `sessionId` and a non-empty `prompt` array are required. */
function parseSteerRequest(params: unknown): SteerRequest {
if (!params || typeof params !== "object") {
throw RequestError.invalidParams(undefined, "steer params must be an object");
}
const { sessionId, prompt, _meta } = params as Record<string, unknown>;
if (typeof sessionId !== "string" || sessionId.length === 0) {
throw RequestError.invalidParams(undefined, "steer params require a non-empty sessionId");
}
if (!Array.isArray(prompt) || prompt.length === 0) {
throw RequestError.invalidParams(undefined, "steer params require a non-empty prompt array");
}
const steering =
_meta && typeof _meta === "object" ? (_meta as Record<string, unknown>).steering : undefined;
const idleBehavior =
steering && typeof steering === "object"
? (steering as Record<string, unknown>).idleBehavior
: undefined;
if (idleBehavior !== undefined && idleBehavior !== "promptRequired") {
throw RequestError.invalidParams(undefined, "unsupported steering idleBehavior");
}
return {
sessionId,
prompt: prompt as PromptRequest["prompt"],
_meta: _meta as SteerMeta | null | undefined,
};
}
/** Who started a turn. `idleCompaction` is the adapter's own `/compact` (see
* `IdleCompaction`): it is not a user prompt, so it neither disarms idle
* compaction, asks for a title from its prompt, nor clears the published prompt suggestion. */

35
src/v1/async-task-stop.ts Normal file
View File

@@ -0,0 +1,35 @@
import { RequestError } from "@agentclientprotocol/sdk/unstable";
/** Stops one Claude background task without cancelling the parent prompt turn. */
export const ASYNC_TASK_STOP_METHOD = "_session/async_task/stop";
export type AsyncTaskStopRequest = {
sessionId: string;
asyncTaskId: string;
};
export type AsyncTaskStopResponse = {
stopped: boolean;
};
export function parseAsyncTaskStopRequest(value: unknown): AsyncTaskStopRequest {
if (!value || typeof value !== "object" || Array.isArray(value)) {
throw RequestError.invalidParams(undefined, "async task stop params must be an object");
}
const params = value as Record<string, unknown>;
const sessionId = typeof params.sessionId === "string" ? params.sessionId.trim() : "";
const asyncTaskId = typeof params.asyncTaskId === "string" ? params.asyncTaskId.trim() : "";
if (!sessionId) {
throw RequestError.invalidParams(
undefined,
"async task stop params require a non-empty sessionId",
);
}
if (!asyncTaskId) {
throw RequestError.invalidParams(
undefined,
"async task stop params require a non-empty asyncTaskId",
);
}
return { sessionId, asyncTaskId };
}

73
src/v1/steering.ts Normal file
View File

@@ -0,0 +1,73 @@
import { PromptRequest, RequestError } from "@agentclientprotocol/sdk/unstable";
/** Custom (extension) request method a client uses to steer the turn that is
* currently running: the message is injected into the in-flight turn rather
* than queued as a separate `session/prompt`. Named `_session/steering` per the
* agreed ACP steering wire protocol; advertised to clients via the top-level
* `InitializeResponse._meta.steering.supported`. */
export const STEER_METHOD = "_session/steering";
/** How urgently the SDK delivers a steered message relative to the running
* turn — an internal Claude implementation detail, not part of the wire
* contract. `now` pre-empts the current generation, while `later` waits for a
* pending permission/elicitation callback to settle instead of cancelling its
* ACP request and hiding the client's user-input card (IJAI-1191). */
export const STEER_PRIORITY_NOW = "now" as const;
export const STEER_PRIORITY_LATER = "later" as const;
/** Request-level steering options. `promptRequired` is opt-in so existing Hosts
* keep the established idle fallback behavior. */
type SteerMeta = {
[key: string]: unknown;
steering?: {
idleBehavior?: "promptRequired";
};
};
/** Params of a {@link STEER_METHOD} request. Shaped like the relevant subset of
* a `PromptRequest` so the same `promptToClaude` conversion applies. Delivery
* priority is deliberately NOT exposed here — it's an internal detail the agent
* chooses (see {@link STEER_PRIORITY}). */
export type SteerRequest = {
sessionId: string;
prompt: PromptRequest["prompt"];
_meta?: SteerMeta | null;
};
/** Result of a {@link STEER_METHOD} request. The legacy `startedNewTurn` result
* remains the default idle behavior; `promptRequired` is returned only when the
* Host explicitly opts into the host-owned fallback in request `_meta`. */
export type SteerResponse =
| { outcome: "injected" }
| { outcome: "startedNewTurn" }
| { outcome: "promptRequired"; reason: "noRunningTurn" };
/** Validate raw JSON-RPC params into a {@link SteerRequest}. Kept minimal — the
* content blocks are handed to `promptToClaude`, which tolerates unknown block
* types — but `sessionId` and a non-empty `prompt` array are required. */
export function parseSteerRequest(params: unknown): SteerRequest {
if (!params || typeof params !== "object") {
throw RequestError.invalidParams(undefined, "steer params must be an object");
}
const { sessionId, prompt, _meta } = params as Record<string, unknown>;
if (typeof sessionId !== "string" || sessionId.length === 0) {
throw RequestError.invalidParams(undefined, "steer params require a non-empty sessionId");
}
if (!Array.isArray(prompt) || prompt.length === 0) {
throw RequestError.invalidParams(undefined, "steer params require a non-empty prompt array");
}
const steering =
_meta && typeof _meta === "object" ? (_meta as Record<string, unknown>).steering : undefined;
const idleBehavior =
steering && typeof steering === "object"
? (steering as Record<string, unknown>).idleBehavior
: undefined;
if (idleBehavior !== undefined && idleBehavior !== "promptRequired") {
throw RequestError.invalidParams(undefined, "unsupported steering idleBehavior");
}
return {
sessionId,
prompt: prompt as PromptRequest["prompt"],
_meta: _meta as SteerMeta | null | undefined,
};
}