콘텐츠로 이동

이벤트

ActiveCli.Event.AbstractEvent — CLI가 우리에게 말한 하나의 사건을, 파싱된 JSON이 아니라 인스턴스로.

abstract class AbstractEvent {
readonly raw: Readonly<Record<string, unknown>>;
get type(): string;
toJSON(): Record<string, unknown>;
}

type은 엔트리에 문자열 type이 없을 때 throw하는 대신 ''로 답합니다.

ActiveCli.Event.EventParser — CLI가 보낸 JSON 한 줄을 그것이 나타내는 이벤트 인스턴스로 바꿉니다. 파싱된 JSON이 plain object로 여행하기를 멈추는 경계입니다. 줄이 완성되는 순간 이것을 호출하고, 그 뒤로는 인스턴스를 전달하십시오.

class EventParser {
parse(line: string): AbstractEvent | null;
classify(raw: Readonly<Record<string, unknown>>): AbstractEvent;
}
입력 결과
{"type":"control_request",…} PermissionRequestEvent
{"type":"system",…} SystemEvent
{"type":"assistant",…} AssistantEvent
{"type":"result",…} ResultEvent
그 밖의 모든 JSON 객체 GenericEvent
JSON이 아님 (Warning: …) null
객체가 아닌 JSON ("str", [1,2], null) null

JSON이 아닌 출력은 오류가 아니라 예상된 잡음입니다. CLI는 경고와 진단을 같은 스트림에 쓰며, 그것을 실패로 취급하는 호스트는 CLI가 말이 많아진 버전에서 깨집니다. null 반환은 호출자가 그 줄을 고장이라 판단하지 않고 건너뛸 수 있게 합니다.

classify()는 이미 파싱된 엔트리에 대한 동일한 디스패치입니다 — 저장된 JSONL을 재생할 때 유용합니다.

ActiveCli.Event.PermissionRequestEvent — CLI가 무언가를 해도 되는지 묻는 것, 그리고 답할 수단.

class PermissionRequestEvent extends AbstractEvent {
static matches(raw: Readonly<Record<string, unknown>>): boolean;
get requestId(): string | null;
get subtype(): string | null;
approve(updatedInput?: Record<string, unknown>): Record<string, unknown>;
deny(reason?: string): Record<string, unknown>;
}

답이 요청 위에 사는 이유는, 응답이 그것이 답하는 request_id와 짝지어질 때만 의미를 갖기 때문입니다. CLI가 둘을 짝짓고, id 없이 보낸 답은 아무 데도 가지 않습니다. 그 id는 요청과 함께 도착했으므로, 요청이야말로 답을 만드는 가장 짧고 정직한 자리입니다.

approve()deny()는 보낼 줄을 보내는 것이 아니라 반환합니다. 이 이벤트는 답의 모양을 알지, 프로세스가 어디 있는지는 알지 못합니다.

event.requestId; // 'req-7'
event.subtype; // 'can_use_tool' — 도구 권한, 승인할 계획, 또는 질문
event.approve();
// { type: 'control_response',
// response: { subtype: 'success', request_id: 'req-7',
// response: { behavior: 'allow', updatedInput: {} } } }
event.approve({ command: 'ls -la' });
// 안쪽 response: { behavior: 'allow', updatedInput: { command: 'ls -la' } }
event.deny('user declined');
// response: { behavior: 'deny', message: 'user declined' }

updatedInput은 CLI가 제안한 것을 대체합니다 — 호스트가 사용자에게 실행 전 명령을 편집하게 하는 방법입니다.

ActiveCli.Event.AssistantEvent — 어시스턴트가 말한 것.

class AssistantEvent extends AbstractEvent {
static matches(raw: Readonly<Record<string, unknown>>): boolean;
get text(): string;
hasText(): boolean;
}

CLI는 텍스트를 message.content 안에 블록 배열로 중첩하며, 그중 일부만이 텍스트입니다. “무엇이라고 말했는가”를 원하는 모든 호출자가 그 순회를 손으로 작성하고 있었습니다. text는 그 순회를 한 번만 해둔 것입니다. 맨 문자열 content도 처리됩니다 — 그쪽이 단순한 형태이고 블록이 일반적인 형태입니다.

command.subscribe((event) => {
if (event instanceof AssistantEvent && event.hasText()) {
process.stdout.write(event.text);
}
});

ActiveCli.Event.SystemEvent — CLI가 대화가 아니라 자기 자신에 관해 알리는 것.

class SystemEvent extends AbstractEvent {
static matches(raw: Readonly<Record<string, unknown>>): boolean;
reportedPermissionFlag(): string | null;
}

호스트를 permission 모드와 발맞추게 하는 데 중요한 것은 둘입니다. spawn 시의 init, 그리고 CLI가 스스로 모드를 바꿀 때의 status입니다. 후자 덕분에 승인된 계획이 plan 모드를 벗어나는 일을, 그렇게 만든 도구 호출을 들여다보지 않고도 관측할 수 있습니다.

플래그는 CLI 자신의 철자(auto_edit가 아니라 acceptEdits)로 반환됩니다. 그것을 번역하는 것은 어댑터의 일입니다. 자기 CLI가 어떤 어휘를 쓰는지는 어댑터만 알기 때문입니다 — ClaudePermissionFlag#toMode에 통과시키십시오.

const event = parser.parse('{"type":"system","permissionMode":"acceptEdits"}') as SystemEvent;
event.reportedPermissionFlag(); // 'acceptEdits'
new ClaudePermissionFlag().toMode(event.reportedPermissionFlag());
// PermissionMode.AUTO_EDIT

ActiveCli.Event.ResultEvent — CLI가 턴이 끝났음을 보고하는 것.

class ResultEvent extends AbstractEvent {
static matches(raw: Readonly<Record<string, unknown>>): boolean;
}

자체 클래스를 가질 값어치가 있는 이유는 그 부재가 의미를 갖기 때문입니다. 이것을 방출하지 않고 종료한 프로세스는 비정상적으로 끝난 것이며, 종료 코드만 추적하는 호스트는 그것을 깨끗한 종료와 구분할 수 없습니다.

ActiveCli.Event.GenericEvent — 아직 타입 있는 질문이 없는 엔트리.

class GenericEvent extends AbstractEvent {}

의도적으로 실패가 아닙니다. CLI는 이 라이브러리가 모델링하는 것보다 많은 종류의 엔트리를 방출하고, 릴리스 사이에 새로운 것을 추가합니다. 그것을 표현하기를 거부한다면, CLI가 라이브러리보다 앞서 나갈 때마다 호스트가 조용히 이벤트를 잃게 됩니다. 페이로드는 raw를 통해 온전히 도착합니다.

나중에 하위 클래스가 생기는 것은 raw에 닿는 누구에게도 breaking change가 아닙니다.

이벤트의 거울상이 메시지입니다. ActiveCli.Message.ControlRequest는 CLI의 컨트롤 채널로 보내는 요청입니다. 둘 다 control_request로 이동하므로, 클래스가 이름 붙이는 것은 와이어 타입이 아니라 방향입니다.

abstract class ControlRequest {
protected abstract get subtype(): string;
protected payload(): Record<string, unknown>;
toPayload(requestId: string): Record<string, unknown>;
}
class Interrupt extends ControlRequest {}
class Initialize extends ControlRequest {}
class SetModel extends ControlRequest {
constructor(model: string);
}
new Interrupt().toPayload('req-1');
// { type: 'control_request', request_id: 'req-1', request: { subtype: 'interrupt' } }
new Initialize().toPayload('req-2');
// { type: 'control_request', request_id: 'req-2', request: { subtype: 'initialize' } }
new SetModel('opus').toPayload('req-3');
// { type: 'control_request', request_id: 'req-3',
// request: { subtype: 'set_model', model: 'opus' } }

Initialize는 CLI에게 자신을 서술하라고 요청합니다 — 이 설치본이 아는 슬래시 커맨드, 받아들이는 모델, 이 디렉토리에서 실제로 적용되는 설정. Command.describe()가 보내는 것이 이것이며, 오직 묻기 위해서만 존재하는 세션 위에서 보냅니다. 응답은 나가는 것과 같은 방식으로 중첩됩니다 — 어느 요청이 성공했는지 말하는 봉투가 서술 자체를 감쌉니다.

베이스가 봉투를 소유하고 하위 클래스는 자신의 subtype과 추가 필드만 제공합니다. 그래서 이 클래스는 확장에 열려 있으며, 호스트는 이 라이브러리가 이름 붙이지 않은 컨트롤 subtype에도 릴리스를 기다리지 않고 닿을 수 있습니다.

class Custom extends ControlRequest {
protected override get subtype(): string { return 'something_new'; }
protected override payload(): Record<string, unknown> { return { extra: true }; }
}
session.request(new Custom());