| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174 |
- import * as Tool from "./tool"
- import DESCRIPTION from "./task.txt"
- import { Session } from "@/session/session"
- import { SessionID, MessageID } from "../session/schema"
- import { MessageV2 } from "../session/message-v2"
- import { Agent } from "../agent/agent"
- import { deriveSubagentSessionPermission } from "../agent/subagent-permissions"
- import type { SessionPrompt } from "../session/prompt"
- import { Config } from "@/config/config"
- import { Effect, Exit, Schema } from "effect"
- import { EffectBridge } from "@/effect/bridge"
- export interface TaskPromptOps {
- cancel(sessionID: SessionID): Effect.Effect<void>
- resolvePromptParts(template: string): Effect.Effect<SessionPrompt.PromptInput["parts"]>
- prompt(input: SessionPrompt.PromptInput): Effect.Effect<MessageV2.WithParts>
- }
- const id = "task"
- export const Parameters = Schema.Struct({
- description: Schema.String.annotate({ description: "A short (3-5 words) description of the task" }),
- prompt: Schema.String.annotate({ description: "The task for the agent to perform" }),
- subagent_type: Schema.String.annotate({ description: "The type of specialized agent to use for this task" }),
- task_id: Schema.optional(Schema.String).annotate({
- description:
- "This should only be set if you mean to resume a previous task (you can pass a prior task_id and the task will continue the same subagent session as before instead of creating a fresh one)",
- }),
- command: Schema.optional(Schema.String).annotate({ description: "The command that triggered this task" }),
- })
- export const TaskTool = Tool.define(
- id,
- Effect.gen(function* () {
- const agent = yield* Agent.Service
- const config = yield* Config.Service
- const sessions = yield* Session.Service
- const run = Effect.fn("TaskTool.execute")(function* (
- params: Schema.Schema.Type<typeof Parameters>,
- ctx: Tool.Context,
- ) {
- const cfg = yield* config.get()
- if (!ctx.extra?.bypassAgentCheck) {
- yield* ctx.ask({
- permission: id,
- patterns: [params.subagent_type],
- always: ["*"],
- metadata: {
- description: params.description,
- subagent_type: params.subagent_type,
- },
- })
- }
- const next = yield* agent.get(params.subagent_type)
- if (!next) {
- return yield* Effect.fail(new Error(`Unknown agent type: ${params.subagent_type} is not a valid agent type`))
- }
- const taskID = params.task_id
- const session = taskID
- ? yield* sessions.get(SessionID.make(taskID)).pipe(Effect.catchCause(() => Effect.succeed(undefined)))
- : undefined
- const parent = yield* sessions.get(ctx.sessionID)
- const parentAgent = parent.agent
- ? yield* agent.get(parent.agent).pipe(Effect.catchCause(() => Effect.succeed(undefined)))
- : undefined
- const nextSession =
- session ??
- (yield* sessions.create({
- parentID: ctx.sessionID,
- title: params.description + ` (@${next.name} subagent)`,
- permission: [
- ...deriveSubagentSessionPermission({
- parentSessionPermission: parent.permission ?? [],
- parentAgent,
- subagent: next,
- }),
- ...(cfg.experimental?.primary_tools?.map((item) => ({
- pattern: "*",
- action: "allow" as const,
- permission: item,
- })) ?? []),
- ],
- }))
- const msg = yield* Effect.sync(() => MessageV2.get({ sessionID: ctx.sessionID, messageID: ctx.messageID }))
- if (msg.info.role !== "assistant") return yield* Effect.fail(new Error("Not an assistant message"))
- const model = next.model ?? {
- modelID: msg.info.modelID,
- providerID: msg.info.providerID,
- }
- yield* ctx.metadata({
- title: params.description,
- metadata: {
- sessionId: nextSession.id,
- model,
- },
- })
- const ops = ctx.extra?.promptOps as TaskPromptOps
- if (!ops) return yield* Effect.fail(new Error("TaskTool requires promptOps in ctx.extra"))
- const runCancel = yield* EffectBridge.make()
- const messageID = MessageID.ascending()
- const cancel = ops.cancel(nextSession.id)
- function onAbort() {
- runCancel.fork(cancel)
- }
- return yield* Effect.acquireUseRelease(
- Effect.sync(() => {
- ctx.abort.addEventListener("abort", onAbort)
- }),
- () =>
- Effect.gen(function* () {
- const parts = yield* ops.resolvePromptParts(params.prompt)
- const result = yield* ops.prompt({
- messageID,
- sessionID: nextSession.id,
- model: {
- modelID: model.modelID,
- providerID: model.providerID,
- },
- agent: next.name,
- tools: {
- ...(next.permission.some((rule) => rule.permission === "todowrite") ? {} : { todowrite: false }),
- ...(next.permission.some((rule) => rule.permission === id) ? {} : { task: false }),
- ...Object.fromEntries((cfg.experimental?.primary_tools ?? []).map((item) => [item, false])),
- },
- parts,
- })
- return {
- title: params.description,
- metadata: {
- sessionId: nextSession.id,
- model,
- },
- output: [
- `task_id: ${nextSession.id} (for resuming to continue this task if needed)`,
- "",
- "<task_result>",
- result.parts.findLast((item) => item.type === "text")?.text ?? "",
- "</task_result>",
- ].join("\n"),
- }
- }),
- (_, exit) =>
- Effect.gen(function* () {
- if (Exit.hasInterrupts(exit)) yield* cancel
- }).pipe(
- Effect.ensuring(
- Effect.sync(() => {
- ctx.abort.removeEventListener("abort", onAbort)
- }),
- ),
- ),
- )
- })
- return {
- description: DESCRIPTION,
- parameters: Parameters,
- execute: (params: Schema.Schema.Type<typeof Parameters>, ctx: Tool.Context) =>
- run(params, ctx).pipe(Effect.orDie),
- }
- }),
- )
|