From 3429999c35d57d3e0f351b44ab7cb6a98d3b572b Mon Sep 17 00:00:00 2001 From: Rafael Chaves Freitas Date: Fri, 12 Jun 2026 20:19:52 -0300 Subject: [PATCH] feat(tool): allow human-readable slugs as task_id The task tool now accepts human-readable slugs (e.g. "explore-auth") as task_id, making it easy for LLMs to create and resume named task sessions without carrying opaque ses_ IDs across turns. If the slug hasn't been used in the current root session, a new session is created with a derived ID (ses__). If it already exists, the session is resumed with full context. Closes anomalyco/opencode#32118 --- packages/opencode/src/session/session.ts | 15 +++++++++++++++ packages/opencode/src/tool/task.ts | 20 ++++++++++++++++++-- packages/opencode/src/tool/task.txt | 2 +- 3 files changed, 34 insertions(+), 3 deletions(-) diff --git a/packages/opencode/src/session/session.ts b/packages/opencode/src/session/session.ts index 038ec9aa0d6e..6fc03658540e 100644 --- a/packages/opencode/src/session/session.ts +++ b/packages/opencode/src/session/session.ts @@ -255,6 +255,7 @@ export const CreateInput = Schema.optional( metadata: Schema.optional(Metadata), permission: Schema.optional(PermissionV1.Ruleset), workspaceID: Schema.optional(WorkspaceV2.ID), + id: Schema.optional(SessionID), }), ) export type CreateInput = Types.DeepMutable> @@ -469,7 +470,9 @@ export interface Interface { metadata?: typeof Metadata.Type permission?: PermissionV1.Ruleset workspaceID?: WorkspaceV2.ID + id?: SessionID }) => Effect.Effect + readonly root: (sessionID: SessionID) => Effect.Effect readonly fork: (input: { sessionID: SessionID; messageID?: MessageID }) => Effect.Effect readonly touch: (sessionID: SessionID) => Effect.Effect readonly get: (id: SessionID) => Effect.Effect @@ -714,10 +717,12 @@ export const layer: Layer.Layer< metadata?: typeof Metadata.Type permission?: PermissionV1.Ruleset workspaceID?: WorkspaceV2.ID + id?: SessionID }) { const ctx = yield* InstanceState.context const workspace = yield* InstanceState.workspaceID return yield* createNext({ + id: input?.id, parentID: input?.parentID, directory: ctx.directory, path: sessionPath(ctx.worktree, ctx.directory), @@ -730,6 +735,15 @@ export const layer: Layer.Layer< }) }) + const root = Effect.fn("Session.root")(function* (sessionID: SessionID) { + let current = sessionID + while (true) { + const s = yield* get(current) + if (!s.parentID) return current + current = s.parentID + } + }) + const fork = Effect.fn("Session.fork")(function* (input: { sessionID: SessionID; messageID?: MessageID }) { const ctx = yield* InstanceState.context const original = yield* get(input.sessionID) @@ -936,6 +950,7 @@ export const layer: Layer.Layer< list, listGlobal, create, + root, fork, touch, get, diff --git a/packages/opencode/src/tool/task.ts b/packages/opencode/src/tool/task.ts index b0a866c90e23..35119c2e4e3a 100644 --- a/packages/opencode/src/tool/task.ts +++ b/packages/opencode/src/tool/task.ts @@ -14,6 +14,16 @@ import { Effect, Exit, Schema, Scope } from "effect" import { EffectBridge } from "@/effect/bridge" import { RuntimeFlags } from "@/effect/runtime-flags" import { Database } from "@opencode-ai/core/database/database" +import { createHash } from "crypto" + +function isSlug(taskId: string): boolean { + return !taskId.startsWith("ses_") +} + +function deriveSlugSessionID(slug: string, rootID: SessionID): SessionID { + const hash = createHash("sha256").update(rootID).digest("hex").slice(0, 4) + return SessionID.descending(`ses_${hash}_${slug}`) +} export interface TaskPromptOps { cancel(sessionID: SessionID): Effect.Effect @@ -46,7 +56,7 @@ const BaseParameterFields = { 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)", + 'A human-readable slug (e.g. "explore-auth") to create or resume a named task session within this root session. If the slug has not been used yet, a new task is created with that identifier. If it already exists, the existing session is resumed. Also accepts full "ses_..." session IDs to resume a specific session directly.', }), command: Schema.optional(Schema.String).annotate({ description: "The command that triggered this task" }), } @@ -118,8 +128,13 @@ export const TaskTool = Tool.define( return yield* Effect.fail(new Error(`Unknown agent type: ${params.subagent_type} is not a valid agent type`)) } + const slugTaskId = params.task_id && isSlug(params.task_id) ? params.task_id : undefined + const derivedID = slugTaskId + ? deriveSlugSessionID(slugTaskId, yield* sessions.root(ctx.sessionID)) + : undefined + const session = params.task_id - ? yield* sessions.get(SessionID.make(params.task_id)).pipe(Effect.catchCause(() => Effect.succeed(undefined))) + ? yield* sessions.get(derivedID ?? SessionID.make(params.task_id)).pipe(Effect.catchCause(() => Effect.succeed(undefined))) : undefined const parent = yield* sessions.get(ctx.sessionID) const childPermission = deriveSubagentSessionPermission({ @@ -142,6 +157,7 @@ export const TaskTool = Tool.define( const nextSession = session ?? (yield* sessions.create({ + id: derivedID, parentID: ctx.sessionID, title: params.description + ` (@${next.name} subagent)`, agent: next.name, diff --git a/packages/opencode/src/tool/task.txt b/packages/opencode/src/tool/task.txt index c5e412f409d9..126c983cc763 100644 --- a/packages/opencode/src/tool/task.txt +++ b/packages/opencode/src/tool/task.txt @@ -12,7 +12,7 @@ When NOT to use the Task tool: Usage notes: 1. Launch multiple agents concurrently whenever possible, to maximize performance; to do that, use a single message with multiple tool uses 2. Once you have delegated work to an agent, do not duplicate that work yourself. Continue with non-overlapping tasks, or wait for the result. For background tasks, you will be notified automatically when the result is ready. -3. When the agent is done, it will return a single message back to you. The result returned by the agent is not visible to the user. To show the user the result, you should send a text message back to the user with a concise summary of the result. The output includes a task_id you can reuse later to continue the same subagent session. +3. When the agent is done, it will return a single message back to you. The result returned by the agent is not visible to the user. To show the user the result, you should send a text message back to the user with a concise summary of the result. The output includes a task_id you can reuse later to continue the same subagent session. You can also pass a human-readable slug (e.g. "explore-auth") as task_id to create or resume a named task within the current root session — if the slug has not been used yet, a new task is created; if it already exists, the existing session is resumed. 4. Each agent invocation starts with a fresh context unless you provide task_id to resume the same subagent session (which continues with its previous messages and tool outputs). When starting fresh, your prompt should contain a highly detailed task description for the agent to perform autonomously and you should specify exactly what information the agent should return back to you in its final and only message to you. 5. The agent's outputs should generally be trusted 6. Clearly tell the agent whether you expect it to write code or just to do research (search, file reads, web fetches, etc.), since it is not aware of the user's intent. Tell it how to verify its work if possible (e.g., relevant test commands).