Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
34889d2
feat(core): add image_generate tool
rekram1-node Oct 28, 2026
92a9328
fix(core): keep explicit image_generate paths exact
rekram1-node Oct 28, 2026
a3fdab7
refactor(core): drop aspect and resolution from image_generate
rekram1-node Oct 28, 2026
d77d8ea
refactor(core): drop image dimensions from image_generate
rekram1-node Oct 28, 2026
5f8cc25
feat(core): add experimental.image_generation config
rekram1-node Oct 28, 2026
0c64061
feat(core): run image_generate in the background
rekram1-node Oct 28, 2026
71e8de1
feat(core): support model variants in image_generate
rekram1-node Oct 28, 2026
1247cb1
feat(core): describe model types in the models tool
rekram1-node Oct 28, 2026
9590317
feat(core): align image_generate model guidance with subagent
rekram1-node Oct 28, 2026
634880e
feat(core): stronger background wording for image_generate, chat-only…
rekram1-node Oct 28, 2026
d6b5311
feat(core): simpler default image model, webfetch permission for imag…
rekram1-node Oct 29, 2026
ef757c4
feat(core): flatten image config to experimental.image_model
rekram1-node Oct 29, 2026
8e611c9
fix(core): count image jobs as background work for idle hooks
rekram1-node Oct 29, 2026
b9aa4d7
feat(core): let image_generate overwrite an explicit path
rekram1-node Oct 29, 2026
58a0c3e
feat(core): save generated images with their real extension
rekram1-node Oct 29, 2026
9789503
refactor(core): drop redundant checks from image_generate
rekram1-node Oct 29, 2026
68ab1dd
fix(core): ask before replacing a file under the image's real extension
rekram1-node Oct 29, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/client/src/promise/generated/types.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 9 additions & 0 deletions packages/core/src/config/normalize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -495,6 +495,15 @@ function normalizeExperimental(
)
if (value !== undefined) result.media_blob = value
}
if (own(experimental, "image_model")) {
const value = decodeEncoded(
ConfigExperimental.Info.fields.image_model,
experimental.image_model,
["experimental", "image_model"],
diagnostics,
)
if (value !== undefined) result.image_model = value
}
if (own(experimental, "schedules")) {
const value = decodeEncoded(
ConfigExperimental.Info.fields.schedules,
Expand Down
6 changes: 6 additions & 0 deletions packages/core/src/job.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,12 @@ const Background = Schema.Struct({
agent: Schema.String,
description: Schema.String,
}),
Schema.Struct({
kind: Schema.Literal("image"),
sessionID: SessionSchema.ID,
path: Schema.String,
model: Schema.String,
}),
]),
status: Schema.Literals(["running", "completed", "error", "cancelled"]),
output: Schema.optionalKey(Schema.String),
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/location-activity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ export function layer(options: { readonly timeToLive?: Duration.Input; readonly
const owners = [
...(yield* execution.active),
...(yield* jobs.runningBackground).flatMap((recovery) =>
recovery.kind === "shell" ? [recovery.sessionID] : [recovery.childSessionID, recovery.parentSessionID],
recovery.kind === "subagent" ? [recovery.childSessionID, recovery.parentSessionID] : [recovery.sessionID],
),
]
const occupied = new Set(
Expand Down
9 changes: 6 additions & 3 deletions packages/core/src/plugin/goal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,9 +145,12 @@ export const Plugin = define({
if (goal?.status !== "active") return
if (goal.boot !== boot())
return yield* settle(sessionID, goal, "paused", "The server restarted while the goal was active")
// The Session's own running subagent wakes it with its report, and the hook runs again then. A grandchild reports
// to its own parent, so it would never wake this Session.
if (background.some((work) => work.kind === "subagent" && work.sessionID === sessionID)) return
// The Session's own running subagent or image job wakes it with its notice, and the hook runs again then. A
// grandchild reports to its own parent, so it would never wake this Session.
if (
background.some((work) => (work.kind === "subagent" || work.kind === "image") && work.sessionID === sessionID)
)
return
if (goal.rounds >= goal.maxRounds)
return yield* settle(sessionID, goal, "paused", `Reached the limit of ${goal.maxRounds} rounds`)

Expand Down
5 changes: 5 additions & 0 deletions packages/core/src/plugin/internal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import { Environment } from "../environment/index.js"
import { FileAccess } from "../file-access.js"
import { FileMutation } from "../file-mutation.js"
import { Formatter } from "../formatter.js"
import { Generate } from "../generate.js"
import { Form } from "../form.js"
import { FileSystem } from "../filesystem.js"
import { LocationWatcherPolicy } from "../filesystem/location-watcher-policy.js"
Expand Down Expand Up @@ -71,6 +72,7 @@ import { PatchTool } from "../tool/plugin/patch.js"
import { EditTool } from "../tool/plugin/edit.js"
import { GlobTool } from "../tool/plugin/glob.js"
import { GrepTool } from "../tool/plugin/grep.js"
import { ImageGenerateTool } from "../tool/plugin/image-generate.js"
import { McpResourceTools } from "../tool/plugin/mcp-resource.js"
import { OpenCodeTools } from "../tool/plugin/opencode.js"
import { QuestionTool } from "../tool/plugin/question.js"
Expand Down Expand Up @@ -119,6 +121,7 @@ const services = [
FileAccess.Service,
FileMutation.Service,
Formatter.Service,
Generate.Service,
LocationWatcherPolicy.Service,
FileSystem.Service,
FSUtil.Service,
Expand Down Expand Up @@ -172,6 +175,7 @@ export const requirements = LayerNode.group([
FileAccess.node,
FileMutation.node,
Formatter.node,
Generate.node,
LocationWatcherPolicy.node,
FileSystem.node,
FSUtil.node,
Expand Down Expand Up @@ -236,6 +240,7 @@ const pre = [
EditTool.Plugin,
GlobTool.Plugin,
GrepTool.Plugin,
ImageGenerateTool.Plugin,
OpenCodeTools.Plugin,
McpResourceTools.Plugin,
QuestionTool.Plugin,
Expand Down
36 changes: 19 additions & 17 deletions packages/core/src/session/background.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,24 +16,26 @@ export const outstanding = Effect.fnUntraced(function* (
sessionID: SessionSchema.ID,
) {
const owned = yield* Effect.filter(yield* jobs.runningBackground, (recovery) =>
descends(store, recovery.kind === "shell" ? recovery.sessionID : recovery.parentSessionID, sessionID),
)
return owned.map((recovery) =>
recovery.kind === "shell"
? {
kind: "shell" as const,
sessionID: recovery.sessionID,
shellID: recovery.shellID,
command: recovery.command,
}
: {
kind: "subagent" as const,
sessionID: recovery.parentSessionID,
childSessionID: recovery.childSessionID,
agent: recovery.agent,
description: recovery.description,
},
descends(store, recovery.kind === "subagent" ? recovery.parentSessionID : recovery.sessionID, sessionID),
)
return owned.map((recovery) => {
if (recovery.kind === "shell")
return {
kind: "shell" as const,
sessionID: recovery.sessionID,
shellID: recovery.shellID,
command: recovery.command,
}
if (recovery.kind === "image")
return { kind: "image" as const, sessionID: recovery.sessionID, path: recovery.path, model: recovery.model }
return {
kind: "subagent" as const,
sessionID: recovery.parentSessionID,
childSessionID: recovery.childSessionID,
agent: recovery.agent,
description: recovery.description,
}
})
})

const descends = Effect.fnUntraced(function* (
Expand Down
39 changes: 36 additions & 3 deletions packages/core/src/session/execution/restart.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,39 @@ export const layer = (options?: Options) =>
yield* jobs.completeBackground(background.notificationID)
})

const recoverImage = Effect.fnUntraced(function* (
background: Job.Background,
recovery: Extract<Job.Recovery, { kind: "image" }>,
) {
const text =
background.status === "running"
? `Image request for ${recovery.path} was cancelled because the server restarted.`
: background.status === "completed"
? (background.output ?? `Image request finished. Saved image: ${recovery.path} (${recovery.model}).`)
: background.status === "error"
? (background.error ?? `Image request failed for ${recovery.path}.`)
: `Image request for ${recovery.path} was cancelled.`
yield* sessions
.synthetic({
id: background.notificationID,
sessionID: recovery.sessionID,
description: "Image generation",
text,
metadata: {
source: "image_generate",
jobID: background.id,
state: background.status === "running" ? "cancelled" : background.status,
path: recovery.path,
},
resume: false,
})
.pipe(
Effect.catchTag("Session.NotFoundError", () => Effect.void),
Effect.orDie,
)
yield* jobs.completeBackground(background.notificationID)
})

const recoverSubagent = Effect.fnUntraced(function* (
background: Job.Background,
recovery: Extract<Job.Recovery, { kind: "subagent" }>,
Expand Down Expand Up @@ -209,9 +242,9 @@ export const layer = (options?: Options) =>
Effect.fnUntraced(function* (background) {
if ((yield* jobs.get(background.id))?.status === "running") return
const recovery = background.recovery
yield* recovery.kind === "shell"
? recoverShell(background, recovery)
: recoverSubagent(background, recovery, suspended)
if (recovery.kind === "shell") return yield* recoverShell(background, recovery)
if (recovery.kind === "image") return yield* recoverImage(background, recovery)
yield* recoverSubagent(background, recovery, suspended)
}),
{ discard: true },
)
Expand Down
Loading
Loading