agent.ts 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206
  1. import { EOL } from "os"
  2. import { basename } from "path"
  3. import { Cause, Effect } from "effect"
  4. import { Agent } from "../../../agent/agent"
  5. import { Provider } from "@/provider/provider"
  6. import { Session } from "@/session/session"
  7. import type { MessageV2 } from "../../../session/message-v2"
  8. import { MessageID, PartID } from "../../../session/schema"
  9. import { ToolRegistry } from "@/tool/registry"
  10. import { Permission } from "../../../permission"
  11. import { iife } from "../../../util/iife"
  12. import { effectCmd, fail } from "../../effect-cmd"
  13. import { InstanceRef } from "@/effect/instance-ref"
  14. import type { InstanceContext } from "@/project/instance-context"
  15. export const AgentCommand = effectCmd({
  16. command: "agent <name>",
  17. describe: "show agent configuration details",
  18. builder: (yargs) =>
  19. yargs
  20. .positional("name", {
  21. type: "string",
  22. demandOption: true,
  23. description: "Agent name",
  24. })
  25. .option("tool", {
  26. type: "string",
  27. description: "Tool id to execute",
  28. })
  29. .option("params", {
  30. type: "string",
  31. description: "Tool params as JSON or a JS object literal",
  32. }),
  33. handler: Effect.fn("Cli.debug.agent")(function* (args) {
  34. const ctx = yield* InstanceRef
  35. if (!ctx) return
  36. return yield* run(args, ctx)
  37. }),
  38. })
  39. const run = Effect.fn("Cli.debug.agent.body")(function* (
  40. args: { name: string; tool?: string; params?: string },
  41. ctx: InstanceContext,
  42. ) {
  43. const agentName = args.name
  44. const agent = yield* Agent.Service.use((svc) => svc.get(agentName))
  45. if (!agent) {
  46. process.stderr.write(
  47. `Agent ${agentName} not found, run '${basename(process.execPath)} agent list' to get an agent list` + EOL,
  48. )
  49. return yield* fail("", 1)
  50. }
  51. const availableTools = yield* getAvailableTools(agent)
  52. const resolvedTools = resolveTools(agent, availableTools)
  53. const toolID = args.tool
  54. if (toolID) {
  55. const tool = availableTools.find((item) => item.id === toolID)
  56. if (!tool) {
  57. process.stderr.write(`Tool ${toolID} not found for agent ${agentName}` + EOL)
  58. return yield* fail("", 1)
  59. }
  60. if (resolvedTools[toolID] === false) {
  61. process.stderr.write(`Tool ${toolID} is disabled for agent ${agentName}` + EOL)
  62. return yield* fail("", 1)
  63. }
  64. const params = parseToolParams(args.params)
  65. const toolCtx = yield* createToolContext(agent, ctx)
  66. const result = yield* tool.execute(params, toolCtx)
  67. process.stdout.write(JSON.stringify({ tool: toolID, input: params, result }, null, 2) + EOL)
  68. return
  69. }
  70. const output = {
  71. ...agent,
  72. tools: resolvedTools,
  73. }
  74. process.stdout.write(JSON.stringify(output, null, 2) + EOL)
  75. })
  76. const getAvailableTools = Effect.fn("Cli.debug.agent.getAvailableTools")(function* (agent: Agent.Info) {
  77. const provider = yield* Provider.Service
  78. const registry = yield* ToolRegistry.Service
  79. const model =
  80. agent.model ??
  81. (yield* provider.defaultModel().pipe(
  82. Effect.matchCauseEffect({
  83. onSuccess: Effect.succeed,
  84. onFailure: (cause) => {
  85. const error = Cause.squash(cause) as Provider.DefaultModelError
  86. if (error instanceof Provider.ModelNotFoundError) {
  87. return fail(`Model not found: ${error.providerID}/${error.modelID}`)
  88. }
  89. if (error instanceof Provider.NoModelsError) return fail(`No models found for provider ${error.providerID}`)
  90. return fail("No providers found")
  91. },
  92. }),
  93. ))
  94. return yield* registry.tools({ ...model, agent })
  95. })
  96. function resolveTools(agent: Agent.Info, availableTools: { id: string }[]) {
  97. const disabled = Permission.disabled(
  98. availableTools.map((tool) => tool.id),
  99. agent.permission,
  100. )
  101. const resolved: Record<string, boolean> = {}
  102. for (const tool of availableTools) {
  103. resolved[tool.id] = !disabled.has(tool.id)
  104. }
  105. return resolved
  106. }
  107. function parseToolParams(input?: string) {
  108. if (!input) return {}
  109. const trimmed = input.trim()
  110. if (trimmed.length === 0) return {}
  111. const parsed = iife(() => {
  112. try {
  113. return JSON.parse(trimmed)
  114. } catch (jsonError) {
  115. try {
  116. return new Function(`return (${trimmed})`)()
  117. } catch (evalError) {
  118. throw new Error(
  119. `Failed to parse --params. Use JSON or a JS object literal. JSON error: ${jsonError}. Eval error: ${evalError}.`,
  120. { cause: evalError },
  121. )
  122. }
  123. }
  124. })
  125. if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
  126. throw new Error("Tool params must be an object.")
  127. }
  128. return parsed as Record<string, unknown>
  129. }
  130. const createToolContext = Effect.fn("Cli.debug.agent.createToolContext")(function* (
  131. agent: Agent.Info,
  132. ctx: InstanceContext,
  133. ) {
  134. const sessionSvc = yield* Session.Service
  135. const session = yield* sessionSvc.create({ title: `Debug tool run (${agent.name})` })
  136. const messageID = MessageID.ascending()
  137. const model = agent.model
  138. ? agent.model
  139. : yield* Effect.gen(function* () {
  140. const provider = yield* Provider.Service
  141. return yield* provider.defaultModel().pipe(
  142. Effect.matchCauseEffect({
  143. onSuccess: Effect.succeed,
  144. onFailure: (cause) => {
  145. const error = Cause.squash(cause) as Provider.DefaultModelError
  146. if (error instanceof Provider.ModelNotFoundError) {
  147. return fail(`Model not found: ${error.providerID}/${error.modelID}`)
  148. }
  149. if (error instanceof Provider.NoModelsError)
  150. return fail(`No models found for provider ${error.providerID}`)
  151. return fail("No providers found")
  152. },
  153. }),
  154. )
  155. })
  156. const now = Date.now()
  157. const message: MessageV2.Assistant = {
  158. id: messageID,
  159. sessionID: session.id,
  160. role: "assistant",
  161. time: { created: now },
  162. parentID: messageID,
  163. modelID: model.modelID,
  164. providerID: model.providerID,
  165. mode: "debug",
  166. agent: agent.name,
  167. path: {
  168. cwd: ctx.directory,
  169. root: ctx.worktree,
  170. },
  171. cost: 0,
  172. tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
  173. }
  174. yield* sessionSvc.updateMessage(message)
  175. const ruleset = Permission.merge(agent.permission, session.permission ?? [])
  176. return {
  177. sessionID: session.id,
  178. messageID,
  179. callID: PartID.ascending(),
  180. agent: agent.name,
  181. abort: new AbortController().signal,
  182. messages: [],
  183. metadata: () => Effect.void,
  184. ask(req: Omit<Permission.Request, "id" | "sessionID" | "tool">) {
  185. return Effect.sync(() => {
  186. for (const pattern of req.patterns) {
  187. const rule = Permission.evaluate(req.permission, pattern, ruleset)
  188. if (rule.action === "deny") {
  189. throw new Permission.DeniedError({ ruleset })
  190. }
  191. }
  192. })
  193. },
  194. }
  195. })