summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--packages/opencode/src/provider/provider.ts2
-rw-r--r--packages/opencode/src/session/index.ts188
-rw-r--r--packages/opencode/src/session/message.ts7
-rw-r--r--packages/opencode/src/session/system.ts75
-rw-r--r--packages/opencode/src/tool/task.ts39
-rw-r--r--packages/opencode/src/tool/tool.ts1
6 files changed, 201 insertions, 111 deletions
diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts
index 6889feb9f..75ab28675 100644
--- a/packages/opencode/src/provider/provider.ts
+++ b/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[]> = {
diff --git a/packages/opencode/src/session/index.ts b/packages/opencode/src/session/index.ts
index 601757e72..ca0ebf447 100644
--- a/packages/opencode/src/session/index.ts
+++ b/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",
diff --git a/packages/opencode/src/session/message.ts b/packages/opencode/src/session/message.ts
index d9ac88d70..d73eee548 100644
--- a/packages/opencode/src/session/message.ts
+++ b/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({
diff --git a/packages/opencode/src/session/system.ts b/packages/opencode/src/session/system.ts
new file mode 100644
index 000000000..5740cce0b
--- /dev/null
+++ b/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]
+ }
+ }
+}
diff --git a/packages/opencode/src/tool/task.ts b/packages/opencode/src/tool/task.ts
new file mode 100644
index 000000000..ce1e1dc0b
--- /dev/null
+++ b/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,
+ }
+ },
+})
diff --git a/packages/opencode/src/tool/tool.ts b/packages/opencode/src/tool/tool.ts
index b573f7581..ccbcaffec 100644
--- a/packages/opencode/src/tool/tool.ts
+++ b/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<