Dax Raad 8 месяцев назад
Родитель
Сommit
a454ba8895

+ 2 - 0
packages/opencode/src/provider/provider.ts

@@ -24,6 +24,7 @@ import { AuthAnthropic } from "../auth/anthropic"
 import { ModelsDev } from "./models"
 import { NamedError } from "../util/error"
 import { Auth } from "../auth"
+import { TaskTool } from "../tool/task"
 
 export namespace Provider {
   const log = Log.create({ service: "provider" })
@@ -298,6 +299,7 @@ export namespace Provider {
     // MultiEditTool,
     WriteTool,
     TodoWriteTool,
+    TaskTool,
     TodoReadTool,
   ]
   const TOOL_MAPPING: Record<string, Tool.Info[]> = {

+ 78 - 110
packages/opencode/src/session/index.ts

@@ -12,24 +12,21 @@ import {
   tool,
   type Tool as AITool,
   type LanguageModelUsage,
+  type UIMessage,
 } from "ai"
 import { z, ZodSchema } from "zod"
 import { Decimal } from "decimal.js"
 
-import PROMPT_ANTHROPIC from "./prompt/anthropic.txt"
-import PROMPT_ANTHROPIC_SPOOF from "./prompt/anthropic_spoof.txt"
-import PROMPT_TITLE from "./prompt/title.txt"
-import PROMPT_SUMMARIZE from "./prompt/summarize.txt"
 import PROMPT_INITIALIZE from "../session/prompt/initialize.txt"
 
 import { Share } from "../share/share"
 import { Message } from "./message"
 import { Bus } from "../bus"
 import { Provider } from "../provider/provider"
-import { SessionContext } from "./context"
-import { ListTool } from "../tool/ls"
 import { MCP } from "../mcp"
 import { NamedError } from "../util/error"
+import type { Tool } from "../tool/tool"
+import { SystemPrompt } from "./system"
 
 export namespace Session {
   const log = Log.create({ service: "session" })
@@ -37,6 +34,7 @@ export namespace Session {
   export const Info = z
     .object({
       id: Identifier.schema("session"),
+      parentID: Identifier.schema("session").optional(),
       share: z
         .object({
           secret: z.string(),
@@ -79,10 +77,11 @@ export namespace Session {
     }
   })
 
-  export async function create() {
+  export async function create(parentID?: string) {
     const result: Info = {
       id: Identifier.descending("session"),
-      title: "New Session - " + new Date().toISOString(),
+      parentID,
+      title: "Child Session - " + new Date().toISOString(),
       time: {
         created: Date.now(),
         updated: Date.now(),
@@ -91,11 +90,12 @@ export namespace Session {
     log.info("created", result)
     state().sessions.set(result.id, result)
     await Storage.writeJSON("session/info/" + result.id, result)
-    share(result.id).then((share) => {
-      update(result.id, (draft) => {
-        draft.share = share
+    if (!result.parentID)
+      share(result.id).then((share) => {
+        update(result.id, (draft) => {
+          draft.share = share
+        })
       })
-    })
     Bus.publish(Event.Updated, {
       info: result,
     })
@@ -186,12 +186,16 @@ export namespace Session {
     providerID: string
     modelID: string
     parts: Message.Part[]
+    system?: string[]
+    tools?: Tool.Info[]
   }) {
     const l = log.clone().tag("session", input.sessionID)
     l.info("chatting")
     const model = await Provider.getModel(input.providerID, input.modelID)
     let msgs = await messages(input.sessionID)
     const previous = msgs.at(-1)
+
+    // auto summarize if too long
     if (previous?.metadata.assistant) {
       const tokens =
         previous.metadata.assistant.tokens.input +
@@ -214,95 +218,25 @@ export namespace Session {
     const lastSummary = msgs.findLast(
       (msg) => msg.metadata.assistant?.summary === true,
     )
-    if (lastSummary)
-      msgs = msgs.filter(
-        (msg) => msg.role === "system" || msg.id >= lastSummary.id,
-      )
+    if (lastSummary) msgs = msgs.filter((msg) => msg.id >= lastSummary.id)
 
+    const app = App.info()
     if (msgs.length === 0) {
-      const app = App.info()
-      if (input.providerID === "anthropic") {
-        const claude: Message.Info = {
-          id: Identifier.ascending("message"),
-          role: "system",
-          parts: [
-            {
-              type: "text",
-              text: PROMPT_ANTHROPIC_SPOOF.trim(),
-            },
-          ],
-          metadata: {
-            sessionID: input.sessionID,
-            time: {
-              created: Date.now(),
-            },
-            tool: {},
-          },
-        }
-        await updateMessage(claude)
-        msgs.push(claude)
-      }
-      const system: Message.Info = {
-        id: Identifier.ascending("message"),
-        role: "system",
-        parts: [
-          {
-            type: "text",
-            text: PROMPT_ANTHROPIC,
-          },
-          {
-            type: "text",
-            text: [
-              `Here is some useful information about the environment you are running in:`,
-              `<env>`,
-              `Working directory: ${app.path.cwd}`,
-              `Is directory a git repo: ${app.git ? "yes" : "no"}`,
-              `Platform: ${process.platform}`,
-              `Today's date: ${new Date().toISOString()}`,
-              `</env>`,
-              `<project>`,
-              `${app.git ? await ListTool.execute({ path: app.path.cwd, ignore: [] }, { sessionID: input.sessionID, abort: abort.signal }).then((x) => x.output) : ""}`,
-              `</project>`,
-            ].join("\n"),
-          },
-        ],
-        metadata: {
-          sessionID: input.sessionID,
-          time: {
-            created: Date.now(),
-          },
-          tool: {},
-        },
-      }
-      const context = await SessionContext.find()
-      if (context) {
-        system.parts.push({
-          type: "text",
-          text: context,
-        })
-      }
-      msgs.push(system)
       generateText({
         maxOutputTokens: 20,
         messages: convertToModelMessages([
-          {
-            role: "system",
-            parts: [
-              {
-                type: "text",
-                text: PROMPT_ANTHROPIC_SPOOF.trim(),
-              },
-            ],
-          },
-          {
-            role: "system",
-            parts: [
-              {
-                type: "text",
-                text: PROMPT_TITLE,
-              },
-            ],
-          },
+          ...SystemPrompt.title(input.providerID).map(
+            (x): UIMessage => ({
+              id: Identifier.ascending("message"),
+              role: "system",
+              parts: [
+                {
+                  type: "text",
+                  text: x,
+                },
+              ],
+            }),
+          ),
           {
             role: "user",
             parts: input.parts,
@@ -317,7 +251,6 @@ export namespace Session {
           })
         })
         .catch(() => {})
-      await updateMessage(system)
     }
     const msg: Message.Info = {
       role: "user",
@@ -334,12 +267,21 @@ export namespace Session {
     await updateMessage(msg)
     msgs.push(msg)
 
+    const system = input.system ?? SystemPrompt.provider(input.providerID)
+    system.push(...(await SystemPrompt.environment(input.sessionID)))
+    system.push(...(await SystemPrompt.custom()))
+
     const next: Message.Info = {
       id: Identifier.ascending("message"),
       role: "assistant",
       parts: [],
       metadata: {
         assistant: {
+          system,
+          path: {
+            cwd: app.path.cwd,
+            root: app.path.root,
+          },
           cost: 0,
           tokens: {
             input: 0,
@@ -358,6 +300,7 @@ export namespace Session {
     }
     await updateMessage(next)
     const tools: Record<string, AITool> = {}
+
     for (const item of await Provider.tools(input.providerID)) {
       tools[item.id.replaceAll(".", "_")] = tool({
         id: item.id as any,
@@ -369,6 +312,7 @@ export namespace Session {
             const result = await item.execute(args, {
               sessionID: input.sessionID,
               abort: abort.signal,
+              messageID: next.id,
             })
             next.metadata!.tool![opts.toolCallId] = {
               ...result.metadata,
@@ -395,6 +339,7 @@ export namespace Session {
         },
       })
     }
+
     for (const [key, item] of Object.entries(await MCP.tools())) {
       const execute = item.execute
       if (!execute) continue
@@ -576,7 +521,21 @@ export namespace Session {
       toolCallStreaming: true,
       abortSignal: abort.signal,
       stopWhen: stepCountIs(1000),
-      messages: convertToModelMessages(msgs),
+      messages: convertToModelMessages([
+        ...system.map(
+          (x): UIMessage => ({
+            id: Identifier.ascending("message"),
+            role: "system",
+            parts: [
+              {
+                type: "text",
+                text: x,
+              },
+            ],
+          }),
+        ),
+        ...msgs,
+      ]),
       temperature: model.info.id === "codex-mini-latest" ? undefined : 0,
       tools: {
         ...(await MCP.tools()),
@@ -618,10 +577,11 @@ export namespace Session {
     const lastSummary = msgs.findLast(
       (msg) => msg.metadata.assistant?.summary === true,
     )?.id
-    const filtered = msgs.filter(
-      (msg) => msg.role !== "system" && (!lastSummary || msg.id >= lastSummary),
-    )
+    const filtered = msgs.filter((msg) => !lastSummary || msg.id >= lastSummary)
     const model = await Provider.getModel(input.providerID, input.modelID)
+    const app = App.info()
+    const system = SystemPrompt.summarize(input.providerID)
+
     const next: Message.Info = {
       id: Identifier.ascending("message"),
       role: "assistant",
@@ -630,6 +590,11 @@ export namespace Session {
         tool: {},
         sessionID: input.sessionID,
         assistant: {
+          system,
+          path: {
+            cwd: app.path.cwd,
+            root: app.path.root,
+          },
           summary: true,
           cost: 0,
           modelID: input.modelID,
@@ -650,15 +615,18 @@ export namespace Session {
       abortSignal: abort.signal,
       model: model.language,
       messages: convertToModelMessages([
-        {
-          role: "system",
-          parts: [
-            {
-              type: "text",
-              text: PROMPT_SUMMARIZE,
-            },
-          ],
-        },
+        ...system.map(
+          (x): UIMessage => ({
+            id: Identifier.ascending("message"),
+            role: "system",
+            parts: [
+              {
+                type: "text",
+                text: x,
+              },
+            ],
+          }),
+        ),
         ...filtered,
         {
           role: "user",

+ 6 - 1
packages/opencode/src/session/message.ts

@@ -133,7 +133,7 @@ export namespace Message {
   export const Info = z
     .object({
       id: z.string(),
-      role: z.enum(["system", "user", "assistant"]),
+      role: z.enum(["user", "assistant"]),
       parts: z.array(Part),
       metadata: z.object({
         time: z.object({
@@ -161,8 +161,13 @@ export namespace Message {
         ),
         assistant: z
           .object({
+            system: z.string().array(),
             modelID: z.string(),
             providerID: z.string(),
+            path: z.object({
+              cwd: z.string(),
+              root: z.string(),
+            }),
             cost: z.number(),
             summary: z.boolean().optional(),
             tokens: z.object({

+ 75 - 0
packages/opencode/src/session/system.ts

@@ -0,0 +1,75 @@
+import { App } from "../app/app"
+import { ListTool } from "../tool/ls"
+import { Filesystem } from "../util/filesystem"
+
+import PROMPT_ANTHROPIC from "./prompt/anthropic.txt"
+import PROMPT_ANTHROPIC_SPOOF from "./prompt/anthropic_spoof.txt"
+import PROMPT_SUMMARIZE from "./prompt/summarize.txt"
+import PROMPT_TITLE from "./prompt/title.txt"
+
+export namespace SystemPrompt {
+  export function provider(providerID: string) {
+    const result = []
+    switch (providerID) {
+      case "anthropic":
+        result.push(PROMPT_ANTHROPIC_SPOOF.trim())
+        result.push(PROMPT_ANTHROPIC)
+        break
+      default:
+        result.push(PROMPT_ANTHROPIC)
+        break
+    }
+    return result
+  }
+
+  export async function environment(sessionID: string) {
+    const app = App.info()
+    return [
+      [
+        `Here is some useful information about the environment you are running in:`,
+        `<env>`,
+        `  Working directory: ${app.path.cwd}`,
+        `  Is directory a git repo: ${app.git ? "yes" : "no"}`,
+        `  Platform: ${process.platform}`,
+        `  Today's date: ${new Date().toDateString()}`,
+        `</env>`,
+        `<project>`,
+        `  ${app.git ? await ListTool.execute({ path: app.path.cwd, ignore: [] }, { sessionID: sessionID, messageID: "", abort: AbortSignal.any([]) }).then((x) => x.output) : ""}`,
+        `</project>`,
+      ].join("\n"),
+    ]
+  }
+
+  const CUSTOM_FILES = [
+    "AGENTS.md",
+    "CLAUDE.md",
+    "CONTEXT.md", // deprecated
+  ]
+  export async function custom() {
+    const { cwd, root } = App.info().path
+    const found = []
+    for (const item of CUSTOM_FILES) {
+      const matches = await Filesystem.findUp(item, cwd, root)
+      found.push(...matches.map((x) => Bun.file(x).text()))
+    }
+    return Promise.all(found)
+  }
+
+  export function summarize(providerID: string) {
+    switch (providerID) {
+      case "anthropic":
+        return [PROMPT_ANTHROPIC_SPOOF.trim(), PROMPT_SUMMARIZE]
+      default:
+        return [PROMPT_SUMMARIZE]
+    }
+  }
+
+  export function title(providerID: string) {
+    switch (providerID) {
+      case "anthropic":
+        return [PROMPT_ANTHROPIC_SPOOF.trim(), PROMPT_TITLE]
+      default:
+        return [PROMPT_TITLE]
+    }
+  }
+}

+ 39 - 0
packages/opencode/src/tool/task.ts

@@ -0,0 +1,39 @@
+import { Tool } from "./tool"
+import DESCRIPTION from "./task.txt"
+import { z } from "zod"
+import { Session } from "../session"
+
+export const TaskTool = Tool.define({
+  id: "opencode.task",
+  description: DESCRIPTION,
+  parameters: z.object({
+    description: z
+      .string()
+      .describe("A short (3-5 words) description of the task"),
+    prompt: z.string().describe("The task for the agent to perform"),
+  }),
+  async execute(params, ctx) {
+    const session = await Session.create(ctx.sessionID)
+    const msg = await Session.getMessage(ctx.sessionID, ctx.messageID)
+    const metadata = msg.metadata.assistant!
+
+    const result = await Session.chat({
+      sessionID: session.id,
+      modelID: metadata.modelID,
+      providerID: metadata.providerID,
+      parts: [
+        {
+          type: "text",
+          text: params.prompt,
+        },
+      ],
+    })
+
+    return {
+      metadata: {
+        title: params.description,
+      },
+      output: result.parts.findLast((x) => x.type === "text")!.text,
+    }
+  },
+})

+ 1 - 0
packages/opencode/src/tool/tool.ts

@@ -7,6 +7,7 @@ export namespace Tool {
   }
   export type Context = {
     sessionID: string
+    messageID: string
     abort: AbortSignal
   }
   export interface Info<