Skip to content
Open
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
37 changes: 37 additions & 0 deletions docs/commands/agents.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ netlify agents
| [`agents:commit`](/commands/agents#agentscommit) | Commit an agent run’s changes directly to a branch |
| [`agents:create`](/commands/agents#agentscreate) | Create and start a new agent run on your site |
| [`agents:diff`](/commands/agents#agentsdiff) | Print the code changes produced by an agent run |
| [`agents:follow-up`](/commands/agents#agentsfollow-up) | Send a follow-up prompt to an existing agent run |
| [`agents:list`](/commands/agents#agentslist) | List agent runs for the current site |
| [`agents:open`](/commands/agents#agentsopen) | Open the agent run preview, dashboard, or pull request in a browser |
| [`agents:pr`](/commands/agents#agentspr) | Open a pull request for an agent run |
Expand All @@ -49,6 +50,7 @@ netlify agents
netlify agents:create --prompt "Add a contact form"
netlify agents:list --status running
netlify agents:show 60c7c3b3e7b4a0001f5e4b3a --watch
netlify agents:follow-up 60c7c3b3e7b4a0001f5e4b3a "Also add tests"
netlify agents:diff 60c7c3b3e7b4a0001f5e4b3a
netlify agents:open 60c7c3b3e7b4a0001f5e4b3a
```
Expand Down Expand Up @@ -191,6 +193,41 @@ netlify agents:diff 60c7c3b3e7b4a0001f5e4b3a --session 70d8... --cumulative
netlify agents:diff 60c7c3b3e7b4a0001f5e4b3a --no-color | less
```

---
## `agents:follow-up`

Send a follow-up prompt to an existing agent run

**Usage**

```bash
netlify agents:follow-up
```

**Arguments**

- id - agent run ID to follow up on
- prompt - the follow-up prompt

**Flags**

- `agent` (*string*) - override agent type for this session
- `attach` (*string*) - attach a file or image (repeatable)
- `filter` (*string*) - For monorepos, specify the name of the application to run the command in
- `json` (*boolean*) - output result as JSON
- `model` (*string*) - override model for this session
- `project` (*string*) - project ID or name (if not in a linked directory)
- `prompt` (*string*) - follow-up prompt
- `debug` (*boolean*) - Print debugging information
- `auth` (*string*) - Netlify auth token - can be used to run this command without logging in

**Examples**

```bash
netlify agents:follow-up 60c7c3b3e7b4a0001f5e4b3a "Also add tests"
netlify agents:follow-up 60c7c3b3e7b4a0001f5e4b3a -p "Fix the lint error"
```

---
## `agents:list`

Expand Down
1 change: 1 addition & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ Manage Netlify AI agent runs
| [`agents:commit`](/commands/agents#agentscommit) | Commit an agent run’s changes directly to a branch |
| [`agents:create`](/commands/agents#agentscreate) | Create and start a new agent run on your site |
| [`agents:diff`](/commands/agents#agentsdiff) | Print the code changes produced by an agent run |
| [`agents:follow-up`](/commands/agents#agentsfollow-up) | Send a follow-up prompt to an existing agent run |
| [`agents:list`](/commands/agents#agentslist) | List agent runs for the current site |
| [`agents:open`](/commands/agents#agentsopen) | Open the agent run preview, dashboard, or pull request in a browser |
| [`agents:pr`](/commands/agents#agentspr) | Open a pull request for an agent run |
Expand Down
155 changes: 155 additions & 0 deletions src/commands/agents/agents-follow-up.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
import type { OptionValues } from 'commander'
import inquirer from 'inquirer'

import { chalk, log, logAndThrowError, logJson } from '../../utils/command-helpers.js'
import { startSpinner, stopSpinner } from '../../lib/spinner.js'
import type BaseCommand from '../base-command.js'
import { createAgentsApi } from './api.js'
import { uploadAttachments, type UploadedAttachment } from './attachments.js'
import { AVAILABLE_AGENTS, TERMINAL_SESSION_STATES, type AvailableAgent } from './constants.js'
import type { CreateAgentRunnerSessionPayload } from './types.js'
import {
checkModelAvailability,
formatBytes,
formatStatus,
getAgentName,
sanitizePromptText,
validateAgent,
validatePrompt,
} from './utils.js'

interface AgentFollowUpOptions extends OptionValues {
prompt?: string
agent?: string
model?: string
attach?: string[]
json?: boolean
}

export const agentsFollowUp = async (
id: string,
promptArg: string,
options: AgentFollowUpOptions,
command: BaseCommand,
) => {
if (!id) return logAndThrowError('Agent run ID is required')
await command.authenticate()
const { siteInfo } = command.netlify
const api = createAgentsApi(command.netlify)

if (options.attach && options.attach.length > 0 && !siteInfo.account_id) {
return logAndThrowError('Cannot attach files: no account ID is available for this site')
}

let finalPrompt = promptArg || options.prompt
if (!finalPrompt) {
if (options.json) {
return logAndThrowError('A prompt is required. Pass it as the positional argument or via --prompt.')
}
const { promptInput } = await inquirer.prompt<{ promptInput: string }>([
{
type: 'input',
name: 'promptInput',
message: 'What would you like the agent to do next?',
validate: validatePrompt,
},
])
finalPrompt = promptInput
}
const promptValid = validatePrompt(finalPrompt)
if (promptValid !== true) return logAndThrowError(promptValid)

let lastSessionAgent: AvailableAgent | undefined
let lastSessionModel: string | undefined
let recent: import('./types.js').AgentRunnerSession | undefined
try {
const recentSessions = await api.listAgentRunnerSessions(id, { page: 1, per_page: 1, order_by: 'desc' })
if (recentSessions.length > 0) recent = recentSessions[0]
} catch {
// If lookup fails, fall through and let the create call surface the real error.
}

if (recent && !TERMINAL_SESSION_STATES.includes(recent.state as (typeof TERMINAL_SESSION_STATES)[number])) {
log(chalk.yellow('A session is already running on this run. Wait for it to finish or stop it first:'))
log(` ${chalk.cyan(`netlify agents:stop ${id}`)}`)
return logAndThrowError('Cannot create a follow-up while a session is still active')
}

if (recent) {
const previousAgent = recent.agent_config?.agent
if (previousAgent && AVAILABLE_AGENTS.some((entry) => entry.value === previousAgent)) {
lastSessionAgent = previousAgent
}
if (recent.agent_config?.model) lastSessionModel = recent.agent_config.model
}

let agent: AvailableAgent | undefined = lastSessionAgent
if (options.agent) {
const valid = validateAgent(options.agent)
if (valid !== true) return logAndThrowError(valid)
agent = options.agent as AvailableAgent
}
const model = options.model ?? lastSessionModel
if (model && agent) {
const valid = await checkModelAvailability(api, agent, model)
if (valid !== true) log(chalk.yellow(`⚠ ${valid}`))
}

let attachments: UploadedAttachment[] = []
if (options.attach && options.attach.length > 0 && siteInfo.account_id) {
const uploadSpinner = startSpinner({ text: `Uploading ${options.attach.length.toString()} attachment(s)...` })
try {
attachments = await uploadAttachments(api, siteInfo.account_id, options.attach)
stopSpinner({ spinner: uploadSpinner })
for (const file of attachments) {
log(` ${chalk.green('✓')} ${file.filename} ${chalk.dim(`(${formatBytes(file.size)})`)}`)
}
} catch (error_) {
stopSpinner({ spinner: uploadSpinner, error: true })
const error = error_ as Error
return logAndThrowError(error.message)
}
}

const payload: CreateAgentRunnerSessionPayload = {
prompt: sanitizePromptText(finalPrompt),
agent,
model,
file_keys: attachments.length > 0 ? attachments.map((entry) => entry.fileKey) : undefined,
}

const spinner = startSpinner({ text: 'Sending follow-up prompt...' })
try {
const session = await api.createAgentRunnerSession(id, payload)
stopSpinner({ spinner })

if (options.json) {
logJson(session)
return session
}

log(`${chalk.green('✓')} Follow-up session created!`)
log()
log(chalk.bold('Details:'))
log(` Run ID: ${chalk.cyan(id)}`)
log(` Session ID: ${chalk.cyan(session.id)}`)
log(` Prompt: ${chalk.dim(finalPrompt)}`)
if (agent) log(` Agent: ${chalk.cyan(getAgentName(agent))}${model ? ` (${model})` : ''}`)
log(` Status: ${formatStatus(session.state)}`)
log()
log(chalk.bold('Monitor progress:'))
log(` Watch: ${chalk.cyan(`netlify agents:show ${id} --watch`)}`)
log(` Show: ${chalk.cyan(`netlify agents:show ${id}`)}`)
return session
} catch (error_) {
stopSpinner({ spinner, error: true })
const error = error_ as Error & { status?: number }
if (error.status === 404) return logAndThrowError(`Agent run not found: ${id}`)
if (error.message.toLowerCase().includes('active session')) {
log()
log(chalk.yellow('A session is already running on this run. Wait for it to finish or stop it first:'))
log(` ${chalk.cyan(`netlify agents:stop ${id}`)}`)
}
return logAndThrowError(`Failed to send follow-up: ${error.message}`)
}
}
22 changes: 22 additions & 0 deletions src/commands/agents/agents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,27 @@ export const createAgentsCommand = (program: BaseCommand) => {
await agentsCreate(prompt, options, command)
})

program
.command('agents:follow-up')
.argument('<id>', 'agent run ID to follow up on')
.argument('[prompt]', 'the follow-up prompt')
.description('Send a follow-up prompt to an existing agent run')
.option('-p, --prompt <prompt>', 'follow-up prompt')
.option('-a, --agent <agent>', 'override agent type for this session')
.option('-m, --model <model>', 'override model for this session')
.option('--attach <path>', 'attach a file or image (repeatable)', collect, [])
.option('--project <project>', 'project ID or name (if not in a linked directory)')
.option('--json', 'output result as JSON')
.hook('preAction', requiresSiteInfoWithProject)
.addExamples([
'netlify agents:follow-up 60c7c3b3e7b4a0001f5e4b3a "Also add tests"',
'netlify agents:follow-up 60c7c3b3e7b4a0001f5e4b3a -p "Fix the lint error"',
])
.action(async (id: string, prompt: string, options: OptionValues, command: BaseCommand) => {
const { agentsFollowUp } = await import('./agents-follow-up.js')
await agentsFollowUp(id, prompt, options, command)
})

program
.command('agents:list')
.description('List agent runs for the current site')
Expand Down Expand Up @@ -277,6 +298,7 @@ Note: Agent runs execute remotely on Netlify infrastructure, not locally.`,
'netlify agents:create --prompt "Add a contact form"',
'netlify agents:list --status running',
'netlify agents:show 60c7c3b3e7b4a0001f5e4b3a --watch',
'netlify agents:follow-up 60c7c3b3e7b4a0001f5e4b3a "Also add tests"',
'netlify agents:diff 60c7c3b3e7b4a0001f5e4b3a',
'netlify agents:open 60c7c3b3e7b4a0001f5e4b3a',
])
Expand Down
Loading
Loading