Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
10 changes: 8 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,14 @@ new version heading in the same commit.

## [Unreleased]

## [0.138.4] — 2026-07-13
### Fixed
## [0.139.0] — 2026-07-13
### Added
- **`image_edit` gains a `remove-background` preset.** Alongside prompt-guided edit and upscale, agents can
now cut out an image's subject with `image_edit({ image, operation: "remove-background" })` — no prompt
needed. It returns a **transparent PNG** saved as a new Library image (source untouched), via Atlas's
dedicated `youchuan/v8.1/remove-background` model. Same governed `generateImage` submit+poll as the other
edit modes (`operation` takes precedence over `scale`/`prompt`), classified `image.edit` (money-capped),
audited `image.edited` (op=remove-background). Verified live (→ 822 KB transparent PNG).
- **An invalid/partial default image model no longer silently breaks generation.** A half-typed default
(e.g. `google/` left in the Settings field) used to fail every `image_generate`/`image_edit` with a
cryptic Atlas "not found". Now guarded two ways: (1) **console warning** — the default-model fields flag
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

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

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "agent-os",
"version": "0.138.4",
"version": "0.139.0",
"description": "A generic, governed operating system for running autonomous agents safely across brands. Ships with a local web console.",
"license": "MIT",
"type": "commonjs",
Expand Down
25 changes: 16 additions & 9 deletions src/edge/image-gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,15 +41,17 @@ export interface ImageGenRequest {
n: number;
}

/** Edit or upscale an EXISTING image. `images` are already-resolved inputs the vendor accepts inline
* (data: URLs or http URLs). `scale` set (>1) ⇒ upscale mode (uses the upscaler model, `prompt` ignored);
* otherwise it's a prompt-guided edit (image-to-image). */
/** Edit, upscale, or run a named preset on an EXISTING image. `images` are already-resolved inputs the
* vendor accepts inline (data: URLs or http URLs). Mode precedence: `operation` (a named preset like
* 'remove-background') ⇒ that preset; else `scale` (>1) ⇒ upscale (uses the upscaler model, `prompt`
* ignored); else a prompt-guided edit (image-to-image). */
export interface ImageEditRequest {
images: string[];
prompt?: string;
model?: string;
size?: string;
scale?: number;
operation?: 'remove-background';
}

export interface ImageBackend {
Expand Down Expand Up @@ -184,6 +186,7 @@ interface AtlasPrediction {
const ATLAS_BUILTIN_IMAGE_MODEL = 'google/nano-banana-2/text-to-image';
const ATLAS_BUILTIN_EDIT_MODEL = 'google/nano-banana-2/edit';
const ATLAS_UPSCALE_MODEL = 'atlascloud/image-upscaler';
const ATLAS_BG_REMOVE_MODEL = 'youchuan/v8.1/remove-background';

/** Does this error look like "the model id is wrong" (vs a transient/other failure)? Only these are
* worth retrying with the built-in default — a real content/timeout error should surface as-is. */
Expand Down Expand Up @@ -215,12 +218,16 @@ class AtlasBackend implements ImageBackend {

async editImage(req: ImageEditRequest): Promise<ImageGenResult> {
if (!req.images.length) throw new Error('an input image is required');
const upscale = typeof req.scale === 'number' && req.scale > 1;
const model = req.model?.trim() || (upscale ? this.upscaleModel : this.editModel);
const fallback = upscale ? ATLAS_UPSCALE_MODEL : ATLAS_BUILTIN_EDIT_MODEL;
// Edit and upscale ride the SAME generateImage submit+poll — only the body shape differs: an upscaler
// takes a single `image` + `outscale`; an image-to-image edit takes `images[]` + a `prompt`.
const build = (m: string): Record<string, unknown> => upscale
const bgRemove = req.operation === 'remove-background';
const upscale = !bgRemove && typeof req.scale === 'number' && req.scale > 1;
const model = req.model?.trim() || (bgRemove ? ATLAS_BG_REMOVE_MODEL : upscale ? this.upscaleModel : this.editModel);
const fallback = bgRemove ? ATLAS_BG_REMOVE_MODEL : upscale ? ATLAS_UPSCALE_MODEL : ATLAS_BUILTIN_EDIT_MODEL;
// All three modes ride the SAME generateImage submit+poll — only the body shape differs: an upscaler
// and background-removal take a single `image` (bg-remove returns a transparent PNG, no prompt); an
// image-to-image edit takes `images[]` + a `prompt`.
const build = (m: string): Record<string, unknown> => bgRemove
? { model: m, image: req.images[0] }
: upscale
? { model: m, image: req.images[0], outscale: req.scale, output_format: 'jpeg' }
: { model: m, prompt: req.prompt ?? '', images: req.images, ...(req.size ? { size: req.size } : {}) };
return this.withModelFallback(model, fallback, build);
Expand Down
22 changes: 13 additions & 9 deletions src/memory/memory-mcp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,19 +172,21 @@ const IMAGE_GENERATE_TOOL = {
const IMAGE_EDIT_TOOL = {
name: 'image_edit',
description:
'Edit or upscale an EXISTING image (image-to-image). Use this to transform an image you already have — ' +
'change its style/content ("make the sky purple", "add a hat"), or upscale it — WITHOUT redrawing from ' +
'scratch. The source `image` can be a Library artifact id (e.g. from a prior image_generate), a file path ' +
'in your working folder (a file you wrote OR one uploaded into the session), or an http(s) image URL. ' +
'The result is saved as a NEW image in the Library (the source is never changed) and the tool returns the ' +
'new artifact id. Pass `prompt` to describe the edit, OR `scale` (2 or 4) to upscale. Governed ' +
'Edit, upscale, or run a named preset on an EXISTING image. Use this to transform an image you already ' +
'have — change its style/content ("make the sky purple", "add a hat"), upscale it, or remove its ' +
'background — WITHOUT redrawing from scratch. The source `image` can be a Library artifact id (e.g. from a ' +
'prior image_generate), a file path in your working folder (a file you wrote OR one uploaded into the ' +
'session), or an http(s) image URL. The result is saved as a NEW image in the Library (the source is never ' +
'changed) and the tool returns the new artifact id. Pass `prompt` to describe an edit, `scale` (2 or 4) to ' +
'upscale, OR `operation` for a named preset ("remove-background" → a transparent PNG cutout). Governed ' +
'(cost-metered + audited). Requires an Atlas Cloud key.',
inputSchema: {
type: 'object',
properties: {
image: { type: 'string', description: 'The image to edit: a Library artifact id, a file path in your working folder (written or uploaded), or an http(s) image URL.' },
prompt: { type: 'string', description: 'How to change the image (e.g. "turn it into a watercolor painting", "remove the background"). Required unless upscaling.' },
prompt: { type: 'string', description: 'How to change the image (e.g. "turn it into a watercolor painting"). Required unless upscaling or using an `operation` preset.' },
scale: { type: 'number', description: 'Upscale factor (2 or 4). When set, upscales the image and `prompt` is ignored.' },
operation: { type: 'string', enum: ['remove-background'], description: 'A named preset. "remove-background" removes the background and returns a transparent PNG (no `prompt` needed). Takes precedence over `scale`/`prompt`.' },
model: { type: 'string', description: 'Optional model id override (an Atlas image-to-image / upscaler model). Omit for the default.' },
},
required: ['image'],
Expand Down Expand Up @@ -1117,10 +1119,12 @@ async function imageGenerate(args: Record<string, unknown>): Promise<string> {
async function imageEdit(args: Record<string, unknown>): Promise<string> {
const image = String(args.image ?? '').trim();
if (!image) return 'An input `image` is required (a Library artifact id, a working-folder file path, or an image URL).';
const operation = args.operation === 'remove-background' ? 'remove-background' : undefined;
const scale = args.scale !== undefined ? Number(args.scale) : undefined;
const prompt = typeof args.prompt === 'string' ? args.prompt.trim() : '';
if (!(scale && scale > 1) && !prompt) return 'Describe the edit in `prompt`, or pass `scale` (2 or 4) to upscale.';
if (!operation && !(scale && scale > 1) && !prompt) return 'Describe the edit in `prompt`, pass `scale` (2 or 4) to upscale, or set `operation` (e.g. "remove-background").';
const body: Record<string, unknown> = { session: SESSION, image };
if (operation) body.operation = operation;
if (prompt) body.prompt = prompt;
if (scale !== undefined) body.scale = scale;
if (typeof args.model === 'string' && args.model.trim()) body.model = args.model.trim();
Expand All @@ -1133,7 +1137,7 @@ async function imageEdit(args: Record<string, unknown>): Promise<string> {
if (!d.ok) return `Could not edit image: ${d.error ?? 'unknown error'}`;
const list = (d.artifacts ?? []).map((a) => `${a.id} (${a.filename})`).join(', ');
const cost = typeof d.costUsd === 'number' ? ` · ~$${d.costUsd.toFixed(3)}` : '';
const what = scale && scale > 1 ? `Upscaled ${scale}×` : 'Edited';
const what = operation === 'remove-background' ? 'Removed background' : scale && scale > 1 ? `Upscaled ${scale}×` : 'Edited';
const warn = d.warning ? ` ⚠ ${d.warning}` : '';
return `${what} with ${d.model ?? 'the default model'}${cost}. New image saved to the Library: ${list}.${warn}`;
}
Expand Down
1 change: 1 addition & 0 deletions src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -885,6 +885,7 @@ async function handle(os: AgentOS, tm: TerminalManager, autos: Automations, req:
prompt: b.prompt ? String(b.prompt) : undefined,
scale: b.scale !== undefined ? Number(b.scale) : undefined,
model: b.model ? String(b.model) : undefined,
operation: b.operation === 'remove-background' ? 'remove-background' : undefined,
});
return sendJson(res, out.ok ? 200 : 400, out);
}
Expand Down
25 changes: 14 additions & 11 deletions src/terminal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2392,16 +2392,18 @@ export class TerminalManager {
* `generateImage`: classified `image.edit` with an estimated `amountUsd` (money-cap applies), the
* result is `ingest`ed as a NEW `image` artifact (the source is never mutated) + an owner-scoped inbox
* card, audited `image.edited`. The source image is any ref `resolveImageRef` accepts — a Library
* artifact id, a working-folder file (written or terminal-uploaded), or a URL. `scale` (>1) upscales
* (prompt ignored); otherwise `prompt` drives an image-to-image edit. Atlas-only (OpenRouter throws).
* artifact id, a working-folder file (written or terminal-uploaded), or a URL. Mode precedence:
* `operation` (a named preset — 'remove-background', a transparent-PNG cutout, no prompt) ⇒ that preset;
* else `scale` (>1) upscales (prompt ignored); else `prompt` drives an image-to-image edit. Atlas-only.
*/
async editImage(sessionId: string, input: { image: string; prompt?: string; scale?: number; model?: string }): Promise<{ ok: boolean; artifacts?: { id: string; filename: string; mime: string }[]; model?: string; costUsd?: number; warning?: string; error?: string }> {
async editImage(sessionId: string, input: { image: string; prompt?: string; scale?: number; model?: string; operation?: 'remove-background' }): Promise<{ ok: boolean; artifacts?: { id: string; filename: string; mime: string }[]; model?: string; costUsd?: number; warning?: string; error?: string }> {
const agent = this.sessionAgent(sessionId);
if (!agent) return { ok: false, error: 'unknown session' };
if (!this.os.artifacts.enabled) return { ok: false, error: 'artifacts store is disabled (no data home)' };
const upscale = typeof input.scale === 'number' && input.scale > 1;
const bgRemove = input.operation === 'remove-background';
const upscale = !bgRemove && typeof input.scale === 'number' && input.scale > 1;
const prompt = (input.prompt || '').trim();
if (!upscale && !prompt) return { ok: false, error: 'describe the edit in `prompt` (or pass `scale` to upscale)' };
if (!bgRemove && !upscale && !prompt) return { ok: false, error: 'describe the edit in `prompt`, pass `scale` to upscale, or set `operation` (e.g. "remove-background")' };
const imageRef = (input.image || '').trim();
if (!imageRef) return { ok: false, error: 'an input `image` is required — a Library artifact id, a working-folder path, or an image URL' };
const resolved = this.resolveImageRef(agent, imageRef);
Expand All @@ -2415,15 +2417,15 @@ export class TerminalManager {
if (!backend) return { ok: false, error: 'image editing is not configured — set an Atlas Cloud key in Settings → Integrations' };

const estimateUsd = DEFAULT_IMAGE_COST_USD;
const op = upscale ? 'upscale' : 'edit';
const model = input.model?.trim() || (upscale ? 'atlascloud/image-upscaler' : op);
const gate = this.gate(sessionId, agent, 'image.edit', { prompt, model, upscale, amountUsd: estimateUsd }, `${op} an image with ${model}`);
const op = bgRemove ? 'remove-background' : upscale ? 'upscale' : 'edit';
const model = input.model?.trim() || (bgRemove ? 'youchuan/v8.1/remove-background' : upscale ? 'atlascloud/image-upscaler' : op);
const gate = this.gate(sessionId, agent, 'image.edit', { prompt, model, op, amountUsd: estimateUsd }, `${op} an image with ${model}`);
if (gate.decision === 'deny') return { ok: false, error: 'blocked by policy' };
if (gate.decision === 'pending') return { ok: false, error: 'this edit needs human approval — an approval request was filed; retry once it is approved' };

let result;
try {
result = await backend.editImage({ images: [resolved.url], prompt, model: input.model, scale: input.scale });
result = await backend.editImage({ images: [resolved.url], prompt, model: input.model, scale: input.scale, operation: input.operation });
} catch (e) {
const msg = e instanceof Error ? e.message : String(e);
this.audit(sessionId, agent, 'image.failed', { model, op, error: msg });
Expand All @@ -2432,15 +2434,16 @@ export class TerminalManager {

const srow = this.db.prepare('SELECT spawned_by, run_as FROM term_sessions WHERE id = ?').get<{ spawned_by: string | null; run_as: string | null }>(sessionId);
const source = srow?.run_as ?? srow?.spawned_by ?? undefined;
const title = upscale ? `Upscaled ${input.scale}×` : (prompt.length > 60 ? prompt.slice(0, 57) + '…' : prompt);
const title = bgRemove ? 'Background removed' : upscale ? `Upscaled ${input.scale}×` : (prompt.length > 60 ? prompt.slice(0, 57) + '…' : prompt);
const description = bgRemove ? `Background removed from ${imageRef}` : upscale ? `Upscaled ${input.scale}× from ${imageRef}` : prompt;
const stamp = Date.now();
const costUsd = result.costUsd ?? estimateUsd;
const perImageUsd = +(costUsd / result.images.length).toFixed(6);
const out: { id: string; filename: string; mime: string }[] = [];
result.images.forEach((img, i) => {
const filename = `${op}-${stamp}${result.images.length > 1 ? `-${i + 1}` : ''}.${img.ext}`;
const r = this.os.artifacts.ingest({
sessionId, agent, source, title, description: upscale ? `Upscaled ${input.scale}× from ${imageRef}` : prompt,
sessionId, agent, source, title, description,
folder: 'edited-images', filename, bytes: img.bytes, kind: 'image', costUsd: perImageUsd,
});
if (!r.ok) return;
Expand Down
Loading