feat: run a linked AI agent's draft when testing a flow, and offer to deploy it (#10993)

* feat(frontend): run a linked agent's draft when testing a flow, and offer to deploy it

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017B4omp8dRgmLitbpQEqFMp

* fix(frontend): settle an agent's autosave before reading it, and refresh its card on a draft save

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017B4omp8dRgmLitbpQEqFMp

* fix(frontend): deploy the agent draft that was validated, and make the draft-tools flag explicit

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017B4omp8dRgmLitbpQEqFMp

* fix(frontend): refuse a stale agent deploy, and warn when a never-deployed agent is kept as a draft

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017B4omp8dRgmLitbpQEqFMp

* docs: record what inlining an agent draft puts in a preview job

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017B4omp8dRgmLitbpQEqFMp

* fix(frontend): name the draft-changes dialog after what it lists

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017B4omp8dRgmLitbpQEqFMp

* fix(frontend): refuse a draft deploy when the draft row is gone

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017B4omp8dRgmLitbpQEqFMp

* fix(frontend): apply the missing-draft refusal to raw apps too

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017B4omp8dRgmLitbpQEqFMp

* fix(frontend): stop reading a deployed resource row as a draft on deploy

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017B4omp8dRgmLitbpQEqFMp

* fix(frontend): do not mistake an outage or a vanished draft for a deploy

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017B4omp8dRgmLitbpQEqFMp

* fix(frontend): refuse an agent read whose pending draft save failed

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017B4omp8dRgmLitbpQEqFMp

* fix(frontend): space the trigger badges and right-align the agent actions

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017B4omp8dRgmLitbpQEqFMp

* fix(frontend): surface a failed agent-draft read instead of dropping it

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017B4omp8dRgmLitbpQEqFMp

* fix(frontend): give the agent draft delete a baseline so a newer edit survives

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017B4omp8dRgmLitbpQEqFMp

* fix(frontend): drop the agent draft cell locally instead of deleting twice

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017B4omp8dRgmLitbpQEqFMp

* test(frontend): pass the withDraft flag the guard tests were missing

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017B4omp8dRgmLitbpQEqFMp

* refactor(frontend): deploy agent drafts the way Review & Deploy does

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017B4omp8dRgmLitbpQEqFMp

* fix(frontend): give the read-only flow graph its own linked-tools bucket

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017B4omp8dRgmLitbpQEqFMp

* fix(frontend): base the resource draft delete on the read that promoted it

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017B4omp8dRgmLitbpQEqFMp

* fix(frontend): refresh every step linking an agent when its draft is saved

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017B4omp8dRgmLitbpQEqFMp

* fix(frontend): write nothing at all when a resource draft has gone

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017B4omp8dRgmLitbpQEqFMp

* test(frontend): pin that the resource draft delete follows its baseline seed

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017B4omp8dRgmLitbpQEqFMp

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
hugocasa
2026-09-07 19:04:38 +02:00
committed by GitHub
co-authored by Claude Opus 5
parent 48a56158c1
commit 7feaf619cf
23 changed files with 1369 additions and 339 deletions
+38 -2
View File
@@ -14,6 +14,14 @@
import JobLoader, { type Callbacks } from './JobLoader.svelte'
import { getStepHistoryLoaderContext } from './stepHistoryLoader.svelte'
import { loadSchemaFromModule } from './flows/flowInfers'
import {
inlineAgentDraft,
loadLinkedAgentDrafts,
normalizeAgentRef,
type LinkedAgentDraft
} from './flows/linkedAgentDrafts'
import { AGENT_FLOW_LOCAL_KEYS } from './flows/agentResourceUtils'
import { sendUserToast } from '$lib/toast'
interface Props {
mod: FlowModule
@@ -138,7 +146,35 @@
} else if (val.type == 'aiagent') {
const { schema } = await loadSchemaFromModule(mod, opWs)
const agentVal = val
// A linked step whose agent has an unsaved draft is tested as the draft, the same way the
// whole-flow preview and the agent editor's own test pane run it. `inlineAgentDraft`
// clears `agent` and moves the draft's brain and tools onto the step, so the branches
// below then treat it as a standalone agent.
let draft: LinkedAgentDraft | undefined
if (val.agent) {
const linked = normalizeAgentRef(val.agent)
try {
draft = (await loadLinkedAgentDrafts([linked], opWs)).get(linked)
} catch (err: any) {
// The load refuses when the agent's unsaved changes cannot be read, and this function's
// caller neither awaits nor catches: without this the rejection is unhandled and the
// button appears to do nothing, with the test already marked as started.
sendUserToast(`Could not run test: ${err?.body ?? err}`, true)
// Guarded like every other access to it here: the entry is only created for steps the
// panel is tracking, and this runs on a path where it may never have been.
if (modulesTestStates.states[mod.id]) {
modulesTestStates.states[mod.id].loading = false
}
return
}
}
const agentVal = draft ? inlineAgentDraft(val, draft.args) : val
// `args` is built from the whole AI agent schema whatever the step is, so on a linked step
// it carries every brain key as undefined even though the form renders only the flow-local
// ones (`flowLocalAgentSchema`). Overlaying those would shadow the brain the draft just
// supplied with nothing, so an inlined step takes only the inputs its form actually offers.
const formKeys = draft ? (AGENT_FLOW_LOCAL_KEYS as readonly string[]) : Object.keys(args)
// The test form only covers the schema it was given, and for a standalone agent that may be
// the flow-local one (the agent editor shows the brain in its own form, not here). Take the
@@ -150,7 +186,7 @@
? {}
: ((agentVal.input_transforms ?? {}) as Record<string, InputTransform>)),
...Object.fromEntries(
Object.keys(args).map((key) => [
formKeys.map((key) => [
key,
{
expr: `flow_input.${key}`,