Skip to content
Source
Sign inSign up

claudeAcp.ts

117 lines3.9 KB

1import * as Option from "effect/Option";
2import * as Schema from "effect/Schema";
3import type * as AcpSchema from "effect-acp/compat";
4import type { AcpToolCallState } from "@t3tools/provider-acp/server/runtimeModel";
5import type {
6 AcpAdapterV2SubagentUpdate,
7 AcpSubagentSessionEvent,
8} from "@t3tools/provider-acp/server/adapter";
9​
10const decodeAgentInput = Schema.decodeUnknownOption(
11 Schema.Struct({
12 prompt: Schema.String,
13 description: Schema.optionalKey(Schema.String),
14 model: Schema.optionalKey(Schema.String),
15 run_in_background: Schema.optionalKey(Schema.Boolean),
16 }),
17);
18const decodeAsyncResponse = Schema.decodeUnknownOption(
19 Schema.Struct({
20 claudeCode: Schema.Struct({ toolResponse: Schema.Struct({ isAsync: Schema.Boolean }) }),
21 }),
22);
23​
24export function isClaudeProvisionalSubagentTool(tool: AcpToolCallState): boolean {
25 return (
26 (tool.data.name === "Agent" || tool.data.name === "Task") &&
27 tool.status !== "failed" &&
28 tool.status !== "completed" &&
29 Option.isNone(decodeAgentInput(tool.data.rawInput))
30 );
32​
33export function extractClaudeSubagentUpdate(
34 tool: AcpToolCallState,
35): AcpAdapterV2SubagentUpdate | undefined {
36 if (tool.data.name !== "Agent" && tool.data.name !== "Task") return;
37 const input = decodeAgentInput(tool.data.rawInput);
38 if (Option.isNone(input)) return;
39 const response = decodeAsyncResponse(tool.data.meta);
40 const background =
41 input.value.run_in_background === true ||
42 (Option.isSome(response) && response.value.claudeCode.toolResponse.isAsync);
43 return {
44 nativeTaskId: tool.toolCallId,
45 childSessionId: null,
46 prompt: input.value.prompt,
47 title: input.value.description ?? null,
48 model: input.value.model ?? null,
49 status:
50 tool.status === "failed"
51 ? "failed"
52 : tool.status === "completed" && !background
53 ? "completed"
54 : "running",
55 result: null,
56 };
58​
59const decodeChildState = Schema.decodeUnknownOption(
60 Schema.Struct({
61 sessionId: Schema.String,
62 state: Schema.Struct({
63 state: Schema.String,
64 stopReason: Schema.optionalKey(Schema.NullOr(Schema.String)),
65 error: Schema.optionalKey(Schema.NullOr(Schema.Struct({ message: Schema.String }))),
66 }),
67 }),
68);
69const decodeDelegation = Schema.decodeUnknownOption(
70 Schema.Struct({
71 messageId: Schema.String,
72 senderSessionId: Schema.optionalKey(Schema.String),
73 content: Schema.Array(
74 Schema.Struct({ type: Schema.String, text: Schema.optionalKey(Schema.String) }),
75 ),
76 }),
77);
78​
79// Claude's session_message id is the Agent/Task or SendMessage tool-use id.
80export function extractClaudeSubagentSessionEvent(
81 notification: AcpSchema.SessionNotification,
82): AcpSubagentSessionEvent | undefined {
83 const update = notification.update;
84 if (update.sessionUpdate !== "_t3_unknown") return;
85 if (update.originalSessionUpdate === "subagent_update") {
86 const child = decodeChildState(update.raw);
87 if (Option.isNone(child)) return;
88 const { sessionId, state } = child.value;
89 return {
90 type: "state",
91 childSessionId: sessionId,
92 parentSessionId: notification.sessionId,
93 status:
94 state.state === "running" || state.state === "requires_action"
95 ? "running"
96 : state.stopReason === "error"
97 ? "failed"
98 : state.stopReason === "cancelled" || state.state === "unknown"
99 ? "cancelled"
100 : "completed",
101 result: state.error?.message ?? null,
102 };
103 }
104 if (update.originalSessionUpdate === "session_message") {
105 const delegation = decodeDelegation(update.raw);
106 if (Option.isNone(delegation)) return;
107 return {
108 type: "delegation",
109 childSessionId: notification.sessionId,
110 parentSessionId: delegation.value.senderSessionId ?? null,
111 delegationId: delegation.value.messageId,
112 prompt: delegation.value.content
113 .flatMap((block) => (block.type === "text" && block.text !== undefined ? [block.text] : []))
114 .join("\n"),
115 };
116 }