Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 0530bb8

Browse files
laststylebender14autofix-ci[bot]tusharmath
authored
feat: allow support for registering custom commands (tailcallhq#503)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Tushar Mathur <[email protected]>
1 parent 771030f commit 0530bb8

16 files changed

Lines changed: 426 additions & 105 deletions

File tree

Cargo.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

README.md

Lines changed: 136 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,11 @@ Forge is a comprehensive coding agent that integrates AI capabilities with your
5353
- [Agent Tools](#agent-tools)
5454
- [Agent Configuration Options](#agent-configuration-options)
5555
- [Built-in Templates](#built-in-templates)
56+
- [Custom Commands](#custom-commands)
5657
- [Example Workflow Configuration](#example-workflow-configuration)
58+
- [Example 1: Using Event Value as Instructions](#example-1-using-event-value-as-instructions)
59+
- [Example 2: Using Event Value as Data in a Template](#example-2-using-event-value-as-data-in-a-template)
60+
- [Comparing the Two Approaches](#comparing-the-two-approaches)
5761
- [Why Shell?](#why-shell)
5862
- [Community](#community)
5963
- [Support Us](#support-us)
@@ -90,7 +94,7 @@ wget -qO- https://raw.githubusercontent.com/antinomyhq/forge/main/install.sh | b
9094
```bash
9195
# Your API key for accessing AI models (see Environment Configuration section)
9296
OPENROUTER_API_KEY=<Enter your Open Router Key>
93-
97+
9498
# Optional: Set a custom URL for OpenAI-compatible providers
9599
#OPENAI_URL=https://custom-openai-provider.com/v1
96100
```
@@ -319,6 +323,7 @@ Forge loads workflow configurations using the following precedence rules:
319323
3. **Default Configuration**: An embedded default configuration is always available as a fallback
320324

321325
When a project configuration exists in the current directory, Forge creates a merged configuration where:
326+
322327
- Project settings in `forge.yaml` take precedence over default settings
323328
- Any settings not specified in the project configuration inherit from defaults
324329

@@ -368,6 +373,7 @@ Each agent needs tools to perform tasks, configured in the `tools` field:
368373
- `user_prompt` - (Optional) Format for user inputs. If not provided, the raw event value is used.
369374

370375
**Example Agent Configuration:**
376+
371377
```yaml
372378
agents:
373379
- id: software-engineer
@@ -389,25 +395,121 @@ Forge provides templates to simplify system prompt creation:
389395

390396
Use these templates with the syntax: `{{> name-of-the-template.hbs }}`
391397

398+
#### Custom Commands
399+
400+
Forge allows you to define custom commands in your workflow configuration. These commands can be executed within the Forge CLI using the `/command_name` syntax.
401+
402+
**Configuration Options:**
403+
404+
- `name` - The name of the command (used as `/name` in the CLI)
405+
- `description` - A description of what the command does
406+
- `value` - (Optional) A default prompt value that will be used if no arguments are provided when executing the command
407+
408+
**Example Custom Command Configuration:**
409+
410+
```yaml
411+
commands:
412+
- name: commit
413+
description: Commit changes with a standard prefix
414+
value: |
415+
Understand the diff produced and commit using the 'conventional commit' standard
416+
417+
- name: branch
418+
description: Create and checkout a new branch
419+
420+
- name: pull-request
421+
description: Create a pull request with standard template
422+
value: |
423+
Understand the diff with respect to `main` and create a pull-request.
424+
Ensure it follows 'conventional commit' standard.
425+
```
426+
427+
With this configuration, users can type `/commit` in the Forge CLI to execute the commit command with the default instructions for handling commits using the conventional commit standard. If specific instructions are needed, they can be provided as an argument: `/commit Create a detailed commit message for the login feature`. Commands without a default value like `/branch` require an argument to be provided: `/branch feature/new-auth`.
428+
429+
**How Custom Commands Work:**
430+
431+
**How Custom Commands Work With the Event System:**
432+
433+
When a custom command is executed in the Forge CLI, it follows a specific event flow:
434+
435+
1. **Command Execution** - User types a command like `/commit feat: add user authentication`
436+
2. **Event Dispatch** - Forge dispatches an event with:
437+
- Name: The command name (e.g., `commit`)
438+
- Value: The provided argument or default value (e.g., `feat: add user authentication`)
439+
3. **Agent Subscription** - Any agent that has subscribed to this event name receives the event
440+
4. **Event Processing** - The agent processes the event according to its configuration
441+
442+
For an agent to respond to a custom command, it must explicitly subscribe to the event with the same name as the command in its configuration. The agent can then use conditional logic in its user prompt to handle different types of events appropriately.
443+
392444
#### Example Workflow Configuration
393445

446+
Forge provides two main approaches for handling custom command events in agents. Below are examples of both approaches.
447+
448+
##### Example 1: Using Event Value as Instructions
449+
450+
In this approach, the event value itself contains complete instructions that are passed directly to the agent:
451+
394452
```yaml
395453
variables:
396454
models:
397455
advanced_model: &advanced_model anthropic/claude-3.7-sonnet
398456
efficiency_model: &efficiency_model anthropic/claude-3.5-haiku
399457

458+
commands:
459+
- name: commit
460+
description: Commit changes with a standard prefix
461+
value: |
462+
Understand the diff produced and commit using the 'conventional commit' standard
463+
464+
- name: pull-request
465+
description: Create a pull request with standard template
466+
value: |
467+
Understand the diff with respect to `main` and create a pull-request.
468+
Ensure it follows 'conventional commit' standard.
469+
400470
agents:
401-
- id: title_generation_worker
402-
model: *efficiency_model
471+
- id: developer
472+
model: *advanced_model
403473
tools:
404-
- tool_forge_event_dispatch
474+
- tool_forge_fs_read
475+
- tool_forge_fs_create
476+
- tool_forge_fs_remove
477+
- tool_forge_fs_patch
478+
- tool_forge_process_shell
479+
- tool_forge_net_fetch
480+
- tool_forge_fs_search
405481
subscribe:
406482
- user_task_init
407-
tool_supported: false # Force XML-based tool call formatting
408-
system_prompt: "{{> system-prompt-title-generator.hbs }}"
409-
user_prompt: <technical_content>{{event.value}}</technical_content>
483+
- user_task_update
484+
- commit # Subscribe to the commit command event
485+
- pull-request # Subscribe to the pull-request command event
486+
ephemeral: false
487+
tool_supported: true
488+
system_prompt: "{{> system-prompt-engineer.hbs }}"
489+
user_prompt: |
490+
<task>{{event.value}}</task>
491+
```
492+
493+
In this example, the entire value from the event is passed directly as the task. The agent receives the complete instructions as they were defined in the command value or provided by the user.
410494
495+
##### Example 2: Using Event Value as Data in a Template
496+
497+
In this approach, the event value is used as data within a template that formats different tasks based on the event name:
498+
499+
```yaml
500+
variables:
501+
models:
502+
advanced_model: &advanced_model anthropic/claude-3.7-sonnet
503+
efficiency_model: &efficiency_model anthropic/claude-3.5-haiku
504+
505+
commands:
506+
- name: commit
507+
description: Create a git commit with the provided message
508+
509+
- name: pull-request
510+
description: Create a pull request with the provided title
511+
512+
agents:
411513
- id: developer
412514
model: *advanced_model
413515
tools:
@@ -421,17 +523,40 @@ agents:
421523
subscribe:
422524
- user_task_init
423525
- user_task_update
526+
- commit # Subscribe to the commit command event
527+
- pull-request # Subscribe to the pull-request command event
424528
ephemeral: false
425-
tool_supported: true # Use model's native tool call format (default)
529+
tool_supported: true
426530
system_prompt: "{{> system-prompt-engineer.hbs }}"
427531
user_prompt: |
532+
{{#if (eq event.name "commit")}}
533+
<task>Create a git commit with the following message: {{event.value}}</task>
534+
{{else if (eq event.name "pull-request")}}
535+
<task>Create a pull request with the title: {{event.value}}</task>
536+
{{else}}
428537
<task>{{event.value}}</task>
538+
{{/if}}
429539
```
430540
431-
This example workflow creates two agents:
541+
This example, the event value is a simpler string that gets embedded within a template. The template uses Handlebars conditional logic (`{{#if (eq event.name "commit")}}`) to format different tasks based on the event name. The event value is used as data within these task templates.
542+
543+
##### Comparing the Two Approaches
544+
545+
**Approach 1: Event Value as Instructions**
546+
547+
- **Best for**: When the command itself represents a complete task or instruction set
548+
- **Flexibility**: Users can provide detailed, multi-line instructions via the command
549+
- **Implementation**: Simpler user_prompt template that just passes the event value through
550+
- **Example use case**: Complex operations where instructions vary significantly
551+
552+
**Approach 2: Event Value as Data**
553+
554+
- **Best for**: When commands follow predictable patterns with varying data points
555+
- **Structure**: More consistent task formatting across different command types
556+
- **Implementation**: More complex user_prompt template with conditional logic
557+
- **Example use case**: Standardized workflows like git operations with varying messages/titles
432558

433-
1. A title generation worker that creates meaningful titles for user conversations
434-
2. A developer agent that can perform comprehensive file and system operations
559+
You can choose the approach that best fits your specific workflow needs. For simple command structures, Approach 2 provides more consistency, while Approach 1 offers greater flexibility for complex operations.
435560

436561
## Why Shell?
437562

crates/forge_api/src/loader.rs

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -69,18 +69,23 @@ impl<F: Infrastructure> ForgeLoaderService<F> {
6969

7070
/// Loads workflow by merging project config with default workflow
7171
async fn load_with_project_config(&self) -> anyhow::Result<Workflow> {
72-
let project_path = Path::new("forge.yaml");
72+
let project_path = Path::new("forge.yaml").canonicalize()?;
7373

7474
let project_content = String::from_utf8(
7575
self.0
7676
.file_read_service()
77-
.read(project_path)
77+
.read(project_path.as_path())
7878
.await?
7979
.to_vec(),
8080
)?;
8181

82-
let project_workflow: Workflow = serde_yaml::from_str(&project_content)
83-
.with_context(|| "Failed to parse project workflow")?;
82+
let project_workflow: Workflow =
83+
serde_yaml::from_str(&project_content).with_context(|| {
84+
format!(
85+
"Failed to parse project workflow: {}",
86+
project_path.display()
87+
)
88+
})?;
8489

8590
// Merge workflows with project taking precedence
8691
let mut merged_workflow = create_default_workflow();

crates/forge_domain/src/context.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -207,9 +207,9 @@ impl Context {
207207
}
208208

209209
pub fn add_message(mut self, content: impl Into<ContextMessage>) -> Self {
210-
let message = content.into();
211-
debug!(message = ?message, "Adding message to context");
212-
self.messages.push(message);
210+
let content = content.into();
211+
debug!(content = ?content, "Adding message to context");
212+
self.messages.push(content);
213213

214214
self
215215
}

crates/forge_domain/src/merge.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ pub mod std {
66

77
pub mod vec {
88

9+
pub use merge::vec::*;
910
use merge::Merge;
1011

1112
use super::Key;

crates/forge_domain/src/workflow.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,25 @@ use crate::{Agent, AgentId};
1212
pub struct Workflow {
1313
#[merge(strategy = crate::merge::vec::unify_by_key)]
1414
pub agents: Vec<Agent>,
15+
16+
#[merge(strategy = crate::merge::option)]
1517
pub variables: Option<HashMap<String, Value>>,
18+
19+
#[merge(strategy = crate::merge::vec::append)]
20+
#[serde(default)]
21+
pub commands: Vec<Command>,
22+
}
23+
24+
#[derive(Default, Debug, Clone, Serialize, Deserialize, Merge, Setters)]
25+
pub struct Command {
26+
#[merge(strategy = crate::merge::std::overwrite)]
27+
pub name: String,
28+
29+
#[merge(strategy = crate::merge::std::overwrite)]
30+
pub description: String,
31+
32+
#[merge(strategy = crate::merge::option)]
33+
pub value: Option<String>,
1634
}
1735

1836
impl Workflow {

crates/forge_main/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ tracing-subscriber.workspace = true
2727
chrono.workspace = true
2828
serde_json.workspace = true
2929
serde.workspace = true
30+
strum.workspace = true
31+
strum_macros.workspace = true
3032

3133
[dev-dependencies]
3234
insta.workspace = true

crates/forge_main/src/banner.rs

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,9 @@ use std::io;
22

33
use colored::Colorize;
44

5-
use crate::model::Command;
6-
75
const BANNER: &str = include_str!("banner");
86

9-
pub fn display() -> io::Result<()> {
10-
let commands = Command::available_commands();
7+
pub fn display(commands: Vec<String>) -> io::Result<()> {
118
// Split the banner into lines and display each line dimmed
129
println!("{} {}", BANNER.dimmed(), commands.join(", ").bold());
1310
Ok(())

crates/forge_main/src/completer/command.rs

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,31 @@
1+
use std::sync::Arc;
2+
13
use reedline::{Completer, Span, Suggestion};
24

3-
use crate::model::Command;
5+
use crate::model::ForgeCommandManager;
6+
7+
#[derive(Clone)]
8+
pub struct CommandCompleter(Arc<ForgeCommandManager>);
49

5-
#[derive(Default)]
6-
pub struct CommandCompleter;
10+
impl CommandCompleter {
11+
pub fn new(command_manager: Arc<ForgeCommandManager>) -> Self {
12+
Self(command_manager)
13+
}
14+
}
715

816
impl Completer for CommandCompleter {
917
fn complete(&mut self, line: &str, _: usize) -> Vec<reedline::Suggestion> {
10-
Command::available_commands()
18+
self.0
19+
.list()
1120
.into_iter()
12-
.filter(|cmd| cmd.starts_with(line))
21+
.filter(|cmd| cmd.name.starts_with(line))
1322
.map(|cmd| Suggestion {
14-
value: cmd,
15-
description: None,
23+
value: cmd.name,
24+
description: Some(cmd.description),
1625
style: None,
1726
extra: None,
1827
span: Span::new(0, line.len()),
19-
append_whitespace: true,
28+
append_whitespace: false,
2029
})
2130
.collect()
2231
}

crates/forge_main/src/completer/input_completer.rs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,23 @@
11
use std::path::PathBuf;
2+
use std::sync::Arc;
23

34
use forge_walker::Walker;
45
use reedline::{Completer, Suggestion};
56

67
use crate::completer::search_term::SearchTerm;
78
use crate::completer::CommandCompleter;
9+
use crate::model::ForgeCommandManager;
810

911
#[derive(Clone)]
1012
pub struct InputCompleter {
1113
walker: Walker,
14+
command: CommandCompleter,
1215
}
1316

1417
impl InputCompleter {
15-
pub fn new(cwd: PathBuf) -> Self {
18+
pub fn new(cwd: PathBuf, command_manager: Arc<ForgeCommandManager>) -> Self {
1619
let walker = Walker::max_all().cwd(cwd).skip_binary(true);
17-
Self { walker }
20+
Self { walker, command: CommandCompleter::new(command_manager) }
1821
}
1922
}
2023

@@ -23,7 +26,7 @@ impl Completer for InputCompleter {
2326
if line.starts_with("/") {
2427
// if the line starts with '/' it's probably a command, so we delegate to the
2528
// command completer.
26-
let result = CommandCompleter.complete(line, pos);
29+
let result = self.command.complete(line, pos);
2730
if !result.is_empty() {
2831
return result;
2932
}

0 commit comments

Comments
 (0)