返回 CodeWhale
core.ts
根目录 / crates / tui / extension-host / src / shims / core.ts
1 /**
2 * `exec.core`: a tool asking the Rust core to run one of the core's tools for it.
3 *
4 * Present on a tool's `exec` only while the call runs under the turn loop's
5 * permission gate: the model called the extension tool directly and the core
6 * gave that one call an invocation ticket (`tool/call.ticket`). A command, a
7 * timer, activation code, a sub-agent's call, and a tool run from inside
8 * `execute_tools` have no ticket, so they have no `exec.core`; the core would
9 * refuse the request anyway, and this is only the early, local answer.
10 *
11 * The host never decides anything here. `core/call` goes through `plan_tool_calls`
12 * and the approval gate exactly like a call the model makes, the approval card
13 * is composed by Rust, and the answer is either the tool's result or a typed
14 * refusal. The ticket is an opaque id the core minted for this invocation; it
15 * is passed back unchanged and never shown to the plugin by any API here.
16 */
17 import { ErrorCode, type Json, type OwnerRef, type ToolResultWire } from '../protocol.ts'
18 import { isJson } from '../json.ts'
19 import { RpcError, type RpcPeer } from '../rpc.ts'
20
21 /** Why the core did not run a call. */
22 export type CoreCallFailure =
23 /** Policy: the core refuses this call outright, or the call's limits are used up. */
24 | 'refused'
25 /** The user declined the approval card. Do not retry. */
26 | 'denied'
27 /** The call was cancelled (the tool's own signal, a revocation, the turn). */
28 | 'cancelled'
29 /** The core could not serve it (the host is going away, the turn ended). */
30 | 'unavailable'
31 /** Anything else, including an input the host refused to send. */
32 | 'failed'
33
34 export class CoreCallError extends Error {
35 constructor(
36 readonly code: CoreCallFailure,
37 message: string,
38 ) {
39 super(message)
40 this.name = 'CoreCallError'
41 }
42 }
43
44 /** What a core tool returned. `isError` is the tool's own failure, not a refusal. */
45 export interface CoreCallResult {
46 content: string
47 isError: boolean
48 structured?: Json
49 }
50
51 export interface CoreApi {
52 /**
53 * Run the core tool `name` with `input` and wait for its result. Rejects with
54 * a `CoreCallError` when the core did not run it. `options.signal` cancels
55 * this call; the tool's own `exec.signal` always does.
56 */
57 call(name: string, input?: Json, options?: { signal?: AbortSignal }): Promise<CoreCallResult>
58 }
59
60 const MAX_NAME_LENGTH = 128
61
62 function failureFor(error: unknown): CoreCallError {
63 if (error instanceof CoreCallError) return error
64 if (error instanceof RpcError) {
65 switch (error.code) {
66 case ErrorCode.Refused:
67 return new CoreCallError('refused', error.message)
68 case ErrorCode.Denied:
69 return new CoreCallError('denied', error.message)
70 case ErrorCode.Cancelled:
71 return new CoreCallError('cancelled', error.message)
72 case ErrorCode.NotAvailable:
73 return new CoreCallError('unavailable', error.message)
74 default:
75 return new CoreCallError('failed', error.message)
76 }
77 }
78 return new CoreCallError('failed', error instanceof Error ? error.message : String(error))
79 }
80
81 function toResult(wire: ToolResultWire): CoreCallResult {
82 const result: CoreCallResult = {
83 content: wire.content.map((block) => block.text).join('\n'),
84 isError: wire.is_error,
85 }
86 if (wire.structured !== undefined) result.structured = wire.structured
87 return result
88 }
89
90 /** The `exec.core` of one `tool/call` that carries `ticket`. Frozen. */
91 export function makeCoreApi(rpc: RpcPeer, owner: OwnerRef, ticket: string, callSignal: AbortSignal): Readonly<CoreApi> {
92 return Object.freeze({
93 async call(name: string, input: Json = {}, options: { signal?: AbortSignal } = {}): Promise<CoreCallResult> {
94 if (typeof name !== 'string' || name.length === 0 || name.length > MAX_NAME_LENGTH) {
95 throw new CoreCallError('failed', `core.call needs a tool name of 1 to ${MAX_NAME_LENGTH} characters`)
96 }
97 if (!isJson(input)) throw new CoreCallError('failed', 'core.call input must be plain JSON')
98 const signal = options.signal === undefined ? callSignal : AbortSignal.any([callSignal, options.signal])
99 try {
100 return toResult(await rpc.request<ToolResultWire>('core/call', { owner, ticket, name, input }, signal))
101 } catch (error) {
102 throw failureFor(error)
103 }
104 },
105 })
106 }
107
107 lines TYPESCRIPT