mirror of
https://github.com/sst/opencode.git
synced 2025-12-23 10:11:41 +00:00
feat(session): add SessionRunner abstraction for async subagent delegation
PR1 of async subagent delegation stack (Issue #5887). This commit introduces the SessionRunner namespace which provides a unified interface for executing session loops: - runOnce(): Synchronous execution wrapping SessionPrompt.prompt (implemented) - runBackground(): Fire-and-forget execution for async delegation (scaffold only) - Options/RunResult types for future background execution - State tracking infrastructure for active background runs No behavior change - this is purely an abstraction layer. Next: PR2 will implement runBackground() with lifecycle events.
This commit is contained in:
parent
855fd07d22
commit
c5cd7c4f2c
2 changed files with 144 additions and 0 deletions
79
packages/opencode/src/session/runner.ts
Normal file
79
packages/opencode/src/session/runner.ts
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
import z from "zod"
|
||||
import { Identifier } from "../id/id"
|
||||
import { Log } from "../util/log"
|
||||
import { SessionPrompt } from "./prompt"
|
||||
import { MessageV2 } from "./message-v2"
|
||||
import { Instance } from "../project/instance"
|
||||
import { fn } from "@/util/fn"
|
||||
|
||||
export namespace SessionRunner {
|
||||
const log = Log.create({ service: "session.runner" })
|
||||
|
||||
export const Options = z
|
||||
.object({
|
||||
model: z.object({
|
||||
providerID: z.string(),
|
||||
modelID: z.string(),
|
||||
}),
|
||||
agent: z.string(),
|
||||
tools: z.record(z.string(), z.boolean()).optional(),
|
||||
origin: z
|
||||
.object({
|
||||
parentSessionID: Identifier.schema("session").optional(),
|
||||
parentMessageID: Identifier.schema("message").optional(),
|
||||
description: z.string().optional(),
|
||||
command: z.string().optional(),
|
||||
})
|
||||
.optional(),
|
||||
timeoutMs: z.number().optional(),
|
||||
maxSteps: z.number().optional(),
|
||||
})
|
||||
.meta({ ref: "SessionRunnerOptions" })
|
||||
export type Options = z.infer<typeof Options>
|
||||
|
||||
export const RunResult = z
|
||||
.object({
|
||||
sessionID: Identifier.schema("session"),
|
||||
message: MessageV2.WithParts,
|
||||
success: z.boolean(),
|
||||
error: z.string().optional(),
|
||||
})
|
||||
.meta({ ref: "SessionRunnerResult" })
|
||||
export type RunResult = z.infer<typeof RunResult>
|
||||
|
||||
const state = Instance.state(() => ({
|
||||
active: {} as Record<
|
||||
string,
|
||||
{
|
||||
startedAt: number
|
||||
options: Options
|
||||
promise: Promise<RunResult>
|
||||
}
|
||||
>,
|
||||
}))
|
||||
|
||||
export function isRunning(id: string): boolean {
|
||||
return id in state().active
|
||||
}
|
||||
|
||||
export function listActive(): string[] {
|
||||
return Object.keys(state().active)
|
||||
}
|
||||
|
||||
export const runOnce = fn(SessionPrompt.PromptInput, async (input): Promise<MessageV2.WithParts> => {
|
||||
log.info("runOnce", { sessionID: input.sessionID, agent: input.agent })
|
||||
return SessionPrompt.prompt(input)
|
||||
})
|
||||
|
||||
export function runBackground(_id: string, _options: Options): void {
|
||||
throw new Error("SessionRunner.runBackground not yet implemented")
|
||||
}
|
||||
|
||||
export function cancelBackground(_id: string): boolean {
|
||||
throw new Error("SessionRunner.cancelBackground not yet implemented")
|
||||
}
|
||||
|
||||
export async function waitFor(_id: string): Promise<RunResult> {
|
||||
throw new Error("SessionRunner.waitFor not yet implemented")
|
||||
}
|
||||
}
|
||||
65
packages/opencode/test/session/runner.test.ts
Normal file
65
packages/opencode/test/session/runner.test.ts
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
import { describe, expect, test } from "bun:test"
|
||||
import { SessionRunner } from "../../src/session/runner"
|
||||
|
||||
describe("SessionRunner", () => {
|
||||
describe("Options schema", () => {
|
||||
test("validates valid options", () => {
|
||||
const valid = {
|
||||
model: { providerID: "anthropic", modelID: "claude-3-5-sonnet" },
|
||||
agent: "code",
|
||||
}
|
||||
expect(SessionRunner.Options.safeParse(valid).success).toBe(true)
|
||||
})
|
||||
|
||||
test("validates options with tools", () => {
|
||||
const opts = {
|
||||
model: { providerID: "anthropic", modelID: "claude-3-5-sonnet" },
|
||||
agent: "code",
|
||||
tools: { bash: true, read: true, write: false },
|
||||
}
|
||||
expect(SessionRunner.Options.safeParse(opts).success).toBe(true)
|
||||
})
|
||||
|
||||
test("validates options with timeout", () => {
|
||||
const opts = {
|
||||
model: { providerID: "openai", modelID: "gpt-4" },
|
||||
agent: "general",
|
||||
timeoutMs: 30000,
|
||||
maxSteps: 10,
|
||||
}
|
||||
expect(SessionRunner.Options.safeParse(opts).success).toBe(true)
|
||||
})
|
||||
|
||||
test("rejects missing model", () => {
|
||||
expect(SessionRunner.Options.safeParse({ agent: "code" }).success).toBe(false)
|
||||
})
|
||||
|
||||
test("rejects missing agent", () => {
|
||||
const opts = { model: { providerID: "anthropic", modelID: "claude-3-5-sonnet" } }
|
||||
expect(SessionRunner.Options.safeParse(opts).success).toBe(false)
|
||||
})
|
||||
|
||||
test("rejects invalid model structure", () => {
|
||||
const opts = { model: { providerID: "anthropic" }, agent: "code" }
|
||||
expect(SessionRunner.Options.safeParse(opts).success).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe("stub methods", () => {
|
||||
test("runBackground throws", () => {
|
||||
const opts: SessionRunner.Options = {
|
||||
model: { providerID: "anthropic", modelID: "claude-3-5-sonnet" },
|
||||
agent: "code",
|
||||
}
|
||||
expect(() => SessionRunner.runBackground("session_123", opts)).toThrow("not yet implemented")
|
||||
})
|
||||
|
||||
test("cancelBackground throws", () => {
|
||||
expect(() => SessionRunner.cancelBackground("session_123")).toThrow("not yet implemented")
|
||||
})
|
||||
|
||||
test("waitFor throws", async () => {
|
||||
await expect(SessionRunner.waitFor("session_123")).rejects.toThrow("not yet implemented")
|
||||
})
|
||||
})
|
||||
})
|
||||
Loading…
Add table
Add a link
Reference in a new issue