Skip to content

Commit e1cb6f3

Browse files
feat(tool): allow human-readable slugs as task_id
Accept a human-readable slug as the task tool's task_id: it is resolved to a deterministic session ID derived from the root session, so passing the same slug resumes the named task and a new slug creates one. Full "ses_..." IDs still resume directly. Adds Session.create({ id }) and Session.root(). Ported from anomalyco#32122 (adapted for the Effect layer). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent b62585e commit e1cb6f3

3 files changed

Lines changed: 34 additions & 3 deletions

File tree

packages/opencode/src/session/session.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,7 @@ export const CreateInput = Schema.optional(
268268
metadata: Schema.optional(Metadata),
269269
permission: Schema.optional(PermissionV1.Ruleset),
270270
workspaceID: Schema.optional(WorkspaceV2.ID),
271+
id: Schema.optional(SessionID),
271272
}),
272273
)
273274
export type CreateInput = Types.DeepMutable<Schema.Schema.Type<typeof CreateInput>>
@@ -425,7 +426,9 @@ export interface Interface {
425426
metadata?: typeof Metadata.Type
426427
permission?: PermissionV1.Ruleset
427428
workspaceID?: WorkspaceV2.ID
429+
id?: SessionID
428430
}) => Effect.Effect<Info>
431+
readonly root: (sessionID: SessionID) => Effect.Effect<SessionID, NotFound>
429432
readonly fork: (input: { sessionID: SessionID; messageID?: MessageID }) => Effect.Effect<Info, NotFound>
430433
readonly touch: (sessionID: SessionID) => Effect.Effect<void>
431434
readonly get: (id: SessionID) => Effect.Effect<Info, NotFound>
@@ -670,10 +673,12 @@ export const layer: Layer.Layer<
670673
metadata?: typeof Metadata.Type
671674
permission?: PermissionV1.Ruleset
672675
workspaceID?: WorkspaceV2.ID
676+
id?: SessionID
673677
}) {
674678
const ctx = yield* InstanceState.context
675679
const workspace = yield* InstanceState.workspaceID
676680
return yield* createNext({
681+
id: input?.id,
677682
parentID: input?.parentID,
678683
directory: ctx.directory,
679684
path: sessionPath(ctx.worktree, ctx.directory),
@@ -686,6 +691,15 @@ export const layer: Layer.Layer<
686691
})
687692
})
688693

694+
const root = Effect.fn("Session.root")(function* (sessionID: SessionID) {
695+
let current = sessionID
696+
while (true) {
697+
const s = yield* get(current)
698+
if (!s.parentID) return current
699+
current = s.parentID
700+
}
701+
})
702+
689703
const fork = Effect.fn("Session.fork")(function* (input: { sessionID: SessionID; messageID?: MessageID }) {
690704
const ctx = yield* InstanceState.context
691705
const original = yield* get(input.sessionID)
@@ -892,6 +906,7 @@ export const layer: Layer.Layer<
892906
list,
893907
listGlobal,
894908
create,
909+
root,
895910
fork,
896911
touch,
897912
get,

packages/opencode/src/tool/task.ts

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,16 @@ import { Effect, Exit, Schema, Scope } from "effect"
1616
import { EffectBridge } from "@/effect/bridge"
1717
import { RuntimeFlags } from "@/effect/runtime-flags"
1818
import { Database } from "@opencode-ai/core/database/database"
19+
import { createHash } from "crypto"
20+
21+
function isSlug(taskId: string): boolean {
22+
return !taskId.startsWith("ses_")
23+
}
24+
25+
function deriveSlugSessionID(slug: string, rootID: SessionID): SessionID {
26+
const hash = createHash("sha256").update(rootID).digest("hex").slice(0, 4)
27+
return SessionID.descending(`ses_${hash}_${slug}`)
28+
}
1929

2030
export interface TaskPromptOps {
2131
cancel(sessionID: SessionID): Effect.Effect<void>
@@ -52,7 +62,7 @@ const BaseParameterFields = {
5262
}),
5363
task_id: Schema.optional(Schema.String).annotate({
5464
description:
55-
"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)",
65+
'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.',
5666
}),
5767
command: Schema.optional(Schema.String).annotate({ description: "The command that triggered this task" }),
5868
}
@@ -153,8 +163,13 @@ export const TaskTool = Tool.define(
153163
return yield* Effect.fail(new Error(`Unknown agent type: ${params.subagent_type} is not a valid agent type`))
154164
}
155165

166+
const slugTaskId = params.task_id && isSlug(params.task_id) ? params.task_id : undefined
167+
const derivedID = slugTaskId ? deriveSlugSessionID(slugTaskId, yield* sessions.root(ctx.sessionID)) : undefined
168+
156169
const session = params.task_id
157-
? yield* sessions.get(SessionID.make(params.task_id)).pipe(Effect.catchCause(() => Effect.succeed(undefined)))
170+
? yield* sessions
171+
.get(derivedID ?? SessionID.make(params.task_id))
172+
.pipe(Effect.catchCause(() => Effect.succeed(undefined)))
158173
: undefined
159174
const parent = yield* sessions.get(ctx.sessionID)
160175
const childPermission = deriveSubagentSessionPermission({
@@ -177,6 +192,7 @@ export const TaskTool = Tool.define(
177192
const nextSession =
178193
session ??
179194
(yield* sessions.create({
195+
id: derivedID,
180196
parentID: ctx.sessionID,
181197
title: params.description + ` (@${next.name} subagent)`,
182198
agent: next.name,

packages/opencode/src/tool/task.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ Model selection:
1818
Usage notes:
1919
1. Launch multiple agents concurrently whenever possible, to maximize performance; to do that, use a single message with multiple tool uses
2020
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.
21-
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.
21+
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.
2222
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.
2323
5. The agent's outputs should generally be trusted
2424
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).

0 commit comments

Comments
 (0)