(id: string, def: ToolDefinition)
| 111 | const custom: Tool.Def[] = [] |
| 112 | |
| 113 | function fromPlugin(id: string, def: ToolDefinition): Tool.Def { |
| 114 | // Plugin tools still expose Zod args publicly; keep that compatibility |
| 115 | // boxed at the registry boundary and give the LLM the original JSON Schema. |
| 116 | // Normalize missing args to `{}` once — pre-1.14.49 the code was |
| 117 | // `z.object(def.args)` and Zod silently tolerated undefined (#27451, #27630). |
| 118 | const args = def.args ?? {} |
| 119 | const entries = Object.entries(args) |
| 120 | const allZod = entries.every((entry) => isZodType(entry[1])) |
| 121 | const zodParams = allZod ? z.object(args) : undefined |
| 122 | const jsonSchema = zodParams ? zodJsonSchema(zodParams) : legacyJsonSchema(entries) |
| 123 | const parameters = zodParams |
| 124 | ? Schema.declare<unknown>((u): u is unknown => zodParams.safeParse(u).success) |
| 125 | : Schema.Unknown |
| 126 | return { |
| 127 | id, |
| 128 | parameters, |
| 129 | jsonSchema, |
| 130 | description: def.description, |
| 131 | execute: (args, toolCtx) => |
| 132 | Effect.gen(function* () { |
| 133 | // Bridge the host's Effect-based `ask` into a Promise-returning |
| 134 | // function for the plugin to make sure context persists |
| 135 | const bridge = yield* EffectBridge.make() |
| 136 | const pluginCtx: PluginToolContext = { |
| 137 | ...toolCtx, |
| 138 | ask: (req) => bridge.promise(toolCtx.ask(req)), |
| 139 | directory: ctx.directory, |
| 140 | worktree: ctx.worktree, |
| 141 | } |
| 142 | const result = yield* Effect.promise(() => def.execute(args as any, pluginCtx)) |
| 143 | const output = typeof result === "string" ? result : result.output |
| 144 | const metadata = typeof result === "string" ? {} : (result.metadata ?? {}) |
| 145 | const attachments = typeof result === "string" ? undefined : result.attachments |
| 146 | const info = yield* agent.get(toolCtx.agent) |
| 147 | const out = yield* truncate.output(output, {}, info) |
| 148 | return { |
| 149 | title: typeof result === "string" ? "" : (result.title ?? ""), |
| 150 | output: out.truncated ? out.content : output, |
| 151 | attachments, |
| 152 | metadata: { |
| 153 | ...metadata, |
| 154 | truncated: out.truncated, |
| 155 | ...(out.truncated && { outputPath: out.outputPath }), |
| 156 | }, |
| 157 | } |
| 158 | }).pipe( |
| 159 | Effect.withSpan("Tool.execute", { |
| 160 | attributes: { |
| 161 | "tool.name": id, |
| 162 | "session.id": toolCtx.sessionID, |
| 163 | "message.id": toolCtx.messageID, |
| 164 | ...(toolCtx.callID ? { "tool.call_id": toolCtx.callID } : {}), |
| 165 | }, |
| 166 | }), |
| 167 | ), |
| 168 | } |
| 169 | } |
| 170 |
no test coverage detected