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
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -222,9 +222,9 @@ Passed as `SessionConfig.ReasoningEffort`. Not all models respect this setting.

### Hooks and User-Input Handlers

The Copilot SDK provides `OnPreToolUse`, `OnPostToolUse`, and `OnUserInputRequest` callbacks on `SessionConfig`. Coralph intentionally does not implement them:
The Copilot SDK exposes tool hooks through `SessionConfig.Hooks` and user-input callbacks through `SessionConfig.OnUserInputRequest`. Coralph intentionally does not configure them:

- **Tool-use hooks** are unnecessary because `CopilotSessionEventRouter` already captures all tool events via `session.On()`.
- **Tool-use hooks** are unnecessary because `CopilotSessionEventRouter` already captures all tool events via `SessionConfig.OnEvent`.
- **User-input handlers** are inappropriate for an unattended loop; models should not prompt for interactive input during an automated run.

### OpenAI-Compatible Providers
Expand Down
8 changes: 4 additions & 4 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ flowchart TD
end

subgraph External["External Dependencies"]
CopilotSDK["GitHub.Copilot.SDK 0.1.32<br/>AI Runtime"]
CopilotSDK["GitHub.Copilot.SDK 1.0.0<br/>AI Runtime"]
Hex1b["Hex1b 0.83.0<br/>TUI Framework"]
ConfigJson["Microsoft.Extensions.Configuration.Json 8.0.0<br/>JSON Config Loading"]
SpectreConsole["Spectre.Console 0.49.1<br/>Rich Terminal UI"]
Expand Down Expand Up @@ -169,7 +169,7 @@ sequenceDiagram
participant Config as ConfigurationService
participant Runner as LoopOrchestrator
participant Tasks as TaskBacklog
participant SDK as GitHub.Copilot.SDK
participant SDK as GitHub.Copilot
participant Output as ConsoleOutput
participant Git as GitService
participant Files as File System
Expand All @@ -194,7 +194,7 @@ sequenceDiagram

| Package | Version | Purpose |
|---------|---------|---------|
| **GitHub.Copilot.SDK** | 0.1.32 | AI runtime for Copilot integration |
| **GitHub.Copilot.SDK** | 1.0.0 | AI runtime for Copilot integration |
| **Hex1b** | 0.83.0 | Interactive split-pane TUI rendering |
| **Microsoft.Extensions.Configuration.Json** | 8.0.0 | Load configuration from JSON files |
| **Microsoft.Extensions.Options.ConfigurationExtensions** | 8.0.0 | Bind configuration to options classes |
Expand All @@ -213,7 +213,7 @@ sequenceDiagram

## Key Design Decisions

1. **Streaming Architecture**: Uses event-based streaming from GitHub.Copilot.SDK for real-time output
1. **Streaming Architecture**: Uses event-based streaming from GitHub.Copilot for real-time output
2. **Pluggable Output Backends**: Runtime logic writes to a single facade; rendering is switched between classic and TUI modes
3. **Stream Compatibility Guardrail**: `--stream-events` forces classic output to preserve JSONL integrations
4. **Tool Extensibility**: Custom AI tools exposed via `AIFunctionFactory.Create()` pattern
Expand Down
44 changes: 30 additions & 14 deletions src/Coralph.Tests/CopilotClientFactoryTests.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
using Coralph;
using GitHub.Copilot.SDK;
using GitHub.Copilot;
using GitHub.Copilot.Rpc;

namespace Coralph.Tests;

#pragma warning disable GHCP001

public class CopilotClientFactoryTests
{
[Fact]
Expand All @@ -20,26 +23,39 @@ public void CreateClientOptions_WithExplicitValues_CopiesKnownFields()

var clientOptions = CopilotClientFactory.CreateClientOptions(options);

Assert.False(string.IsNullOrWhiteSpace(clientOptions.Cwd));
Assert.True(Path.IsPathRooted(clientOptions.Cwd));
Assert.Equal("/usr/local/bin/copilot", clientOptions.CliPath);
Assert.Equal("http://localhost:3000", clientOptions.CliUrl);
Assert.False(string.IsNullOrWhiteSpace(clientOptions.WorkingDirectory));
Assert.True(Path.IsPathRooted(clientOptions.WorkingDirectory));
var connection = Assert.IsType<UriRuntimeConnection>(clientOptions.Connection);
Assert.Equal("http://localhost:3000", connection.Url);
Assert.Null(connection.ConnectionToken);
Assert.Equal("ghp_test_token", clientOptions.GitHubToken);
Assert.NotNull(clientOptions.Telemetry);
Assert.Equal("http://localhost:4318", clientOptions.Telemetry!.OtlpEndpoint);
Assert.Equal("coralph-test", clientOptions.Telemetry.SourceName);
Assert.True(clientOptions.Telemetry.CaptureContent);
}

[Fact]
public void CreateClientOptions_WithCliPath_UsesStdioConnection()
{
var clientOptions = CopilotClientFactory.CreateClientOptions(new LoopOptions
{
CliPath = "/usr/local/bin/copilot"
});

var connection = Assert.IsType<StdioRuntimeConnection>(clientOptions.Connection);
Assert.Equal("/usr/local/bin/copilot", connection.Path);
Assert.Null(connection.Args);
}

[Fact]
public void CreateClientOptions_WithNullOptionalValues_LeavesThemUnset()
{
var clientOptions = CopilotClientFactory.CreateClientOptions(new LoopOptions());

Assert.False(string.IsNullOrWhiteSpace(clientOptions.Cwd));
Assert.True(Path.IsPathRooted(clientOptions.Cwd));
Assert.Null(clientOptions.CliPath);
Assert.Null(clientOptions.CliUrl);
Assert.False(string.IsNullOrWhiteSpace(clientOptions.WorkingDirectory));
Assert.True(Path.IsPathRooted(clientOptions.WorkingDirectory));
Assert.Null(clientOptions.Connection);
Assert.Null(clientOptions.GitHubToken);
Assert.Null(clientOptions.Telemetry);
}
Expand All @@ -48,7 +64,7 @@ public void CreateClientOptions_WithNullOptionalValues_LeavesThemUnset()
public void CreateSessionConfig_WithToolsAndProvider_CopiesExpectedFields()
{
var tools = CustomTools.GetDefaultTools("issues.json", "progress.txt", "generated_tasks.json");
SessionEventHandler onEvent = _ => { };
Action<SessionEvent> onEvent = _ => { };
var options = new LoopOptions
{
Model = "GPT-5.1-Codex",
Expand Down Expand Up @@ -77,10 +93,10 @@ public void CreateSessionConfig_WithToolsAndProvider_CopiesExpectedFields()
Assert.NotNull(config.SystemMessage);
Assert.Equal(SystemMessageMode.Customize, config.SystemMessage!.Mode);
Assert.NotNull(config.SystemMessage.Sections);
Assert.Contains(SystemPromptSections.Tone, config.SystemMessage.Sections.Keys);
Assert.Contains(SystemPromptSections.Guidelines, config.SystemMessage.Sections.Keys);
Assert.Contains(SystemPromptSections.ToolInstructions, config.SystemMessage.Sections.Keys);
Assert.Contains(SystemPromptSections.Safety, config.SystemMessage.Sections.Keys);
Assert.Contains(SystemMessageSection.Tone, config.SystemMessage.Sections.Keys);
Assert.Contains(SystemMessageSection.Guidelines, config.SystemMessage.Sections.Keys);
Assert.Contains(SystemMessageSection.ToolInstructions, config.SystemMessage.Sections.Keys);
Assert.Contains(SystemMessageSection.Safety, config.SystemMessage.Sections.Keys);
}

[Fact]
Expand Down
2 changes: 1 addition & 1 deletion src/Coralph.Tests/CopilotModelDiscoveryTests.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
using System.Text.Json;
using Coralph;
using GitHub.Copilot.SDK;
using GitHub.Copilot;
using Spectre.Console.Testing;

namespace Coralph.Tests;
Expand Down
12 changes: 6 additions & 6 deletions src/Coralph.Tests/CopilotSessionEventRouterTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
using System.Text.Json;
using Coralph;
using Coralph.Ui;
using GitHub.Copilot.SDK;
using GitHub.Copilot;
using Spectre.Console;
using Spectre.Console.Testing;

Expand Down Expand Up @@ -53,7 +53,7 @@
{
MessageId = "message-1",
DeltaContent = "Answer",
ParentToolCallId = "tool-1"

Check warning on line 56 in src/Coralph.Tests/CopilotSessionEventRouterTests.cs

View workflow job for this annotation

GitHub Actions / build-and-test

'AssistantMessageDeltaData.ParentToolCallId' is obsolete: 'This member is deprecated and will be removed in a future version.'
}
});

Expand Down Expand Up @@ -107,10 +107,10 @@
{
ToolCallId = "tool-1",
ToolName = "report_intent",
Arguments = new Dictionary<string, object?>
Arguments = JsonSerializer.SerializeToElement(new Dictionary<string, object?>
{
["intent"] = "test"
}
})
}
});
router.HandleEvent(new ToolExecutionCompleteEvent
Expand Down Expand Up @@ -344,10 +344,10 @@
ToolCallId = "tool-call-1",
Name = "list_open_issues",
Type = AssistantMessageToolRequestType.Function,
Arguments = new Dictionary<string, object?>
Arguments = JsonSerializer.SerializeToElement(new Dictionary<string, object?>
{
["includeClosed"] = false
},
}),
ToolTitle = "List issues",
McpServerName = "coralph",
IntentionSummary = "Read current issue state"
Expand All @@ -364,7 +364,7 @@

Assert.Equal("tool-call-1", request.GetProperty("toolCallId").GetString());
Assert.Equal("list_open_issues", request.GetProperty("name").GetString());
Assert.Equal("Function", request.GetProperty("type").GetString());
Assert.Equal("function", request.GetProperty("type").GetString());
Assert.False(request.GetProperty("arguments").GetProperty("includeClosed").GetBoolean());
Assert.Equal("List issues", request.GetProperty("toolTitle").GetString());
Assert.Equal("coralph", request.GetProperty("mcpServerName").GetString());
Expand Down
12 changes: 6 additions & 6 deletions src/Coralph.Tests/CopilotSystemMessageFactoryTests.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
using Coralph;
using GitHub.Copilot.SDK;
using GitHub.Copilot;

namespace Coralph.Tests;

Expand All @@ -12,10 +12,10 @@ public void Create_UsesCustomizeModeWithExpectedSections()

Assert.Equal(SystemMessageMode.Customize, config.Mode);
Assert.NotNull(config.Sections);
Assert.Contains(SystemPromptSections.Tone, config.Sections.Keys);
Assert.Contains(SystemPromptSections.Guidelines, config.Sections.Keys);
Assert.Contains(SystemPromptSections.ToolInstructions, config.Sections.Keys);
Assert.Contains(SystemPromptSections.Safety, config.Sections.Keys);
Assert.Contains(SystemMessageSection.Tone, config.Sections.Keys);
Assert.Contains(SystemMessageSection.Guidelines, config.Sections.Keys);
Assert.Contains(SystemMessageSection.ToolInstructions, config.Sections.Keys);
Assert.Contains(SystemMessageSection.Safety, config.Sections.Keys);
}

[Fact]
Expand All @@ -24,7 +24,7 @@ public void Create_WithDryRun_AppendsDryRunSafetyInstruction()
var config = CopilotSystemMessageFactory.Create(new LoopOptions { DryRun = true });

Assert.NotNull(config.Sections);
Assert.True(config.Sections.TryGetValue(SystemPromptSections.Safety, out var safety));
Assert.True(config.Sections.TryGetValue(SystemMessageSection.Safety, out var safety));
Assert.NotNull(safety);
Assert.Contains("Dry-run mode is enabled", safety!.Content);
}
Expand Down
14 changes: 9 additions & 5 deletions src/Coralph.Tests/PermissionPolicyTests.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
using GitHub.Copilot.SDK;
using GitHub.Copilot;
using GitHub.Copilot.Rpc;
using Xunit;

namespace Coralph.Tests;

#pragma warning disable GHCP001

public sealed class PermissionPolicyTests
{
[Fact]
Expand Down Expand Up @@ -262,14 +265,15 @@ private static PermissionRequest CreateRequest(string kind)
return new PermissionRequest { Kind = kind };
}

private static void AssertApproved(PermissionRequestResult result)
private static void AssertApproved(PermissionDecision result)
{
Assert.Equal(PermissionRequestResultKind.Approved, result.Kind);
Assert.IsType<PermissionDecisionApproveOnce>(result);
}

private static void AssertRejected(PermissionRequestResult result)
private static void AssertRejected(PermissionDecision result)
{
Assert.Equal(PermissionRequestResultKind.Rejected, result.Kind);
var rejected = Assert.IsType<PermissionDecisionReject>(result);
Assert.Equal("Rejected by Coralph permission policy.", rejected.Feedback);
}

private static PermissionInvocation CreateInvocation()
Expand Down
78 changes: 4 additions & 74 deletions src/Coralph.Tests/packages.lock.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,11 @@
},
"GitHub.Copilot.SDK": {
"type": "Transitive",
"resolved": "0.3.0",
"contentHash": "zTmlbLWsmmZT/v/vbvP66d73t3sJ92gSavCe+52MtD1jwOJsGv5ExL8CY/jxA9/3GQt50LZs3p+ntjBpe/+Rvw==",
"resolved": "1.0.0",
"contentHash": "hAeuf54OVdFxEtf0AlMgiQBNpeMy7rSbr6Y1bJWjeln8U71D5QEryje8auVQATt3l5ubdbmWGFnO9VOnBhWCfw==",
"dependencies": {
"Microsoft.Extensions.AI.Abstractions": "10.2.0",
"Microsoft.Extensions.Logging.Abstractions": "10.0.2",
"StreamJsonRpc": "2.24.84"
"Microsoft.Extensions.Logging.Abstractions": "10.0.2"
}
},
"Hex1b": {
Expand All @@ -58,20 +57,6 @@
"QRCoder": "1.7.0"
}
},
"MessagePack": {
"type": "Transitive",
"resolved": "2.5.198",
"contentHash": "ul2rGISMatBL4AbXTteEml4xtOsKF8yClEkS0rnrMyFs6Cu3KTbdZwOXrcbEUjGFIDiCwHuYIGyS55FeiaYzBw==",
"dependencies": {
"MessagePack.Annotations": "2.5.198",
"Microsoft.NET.StringTools": "17.6.3"
}
},
"MessagePack.Annotations": {
"type": "Transitive",
"resolved": "2.5.198",
"contentHash": "3U9OvqQGTra+Mz1k1zfNAScSdNHobnqtQ51qdMGUZppkNDZJl0X/igq6Qz5zDBLEZoYqZrFtZwFx6wBJHHI8BA=="
},
"Microsoft.CodeCoverage": {
"type": "Transitive",
"resolved": "17.14.1",
Expand Down Expand Up @@ -192,11 +177,6 @@
"resolved": "8.0.0",
"contentHash": "bXJEZrW9ny8vjMF1JV253WeLhpEVzFo1lyaZu1vQ4ZxWUlVvknZ/+ftFgVheLubb4eZPSwwxBeqS1JkCOjxd8g=="
},
"Microsoft.NET.StringTools": {
"type": "Transitive",
"resolved": "18.0.2",
"contentHash": "cTZw3GHkAlqZACYGeQT3niS3UfVQ8CH0O5+zUdhxstrg1Z8Q2ViXYFKjSxHmEXTX85mrOT/QnHZOeQhhSsIrkQ=="
},
"Microsoft.TestPlatform.ObjectModel": {
"type": "Transitive",
"resolved": "17.14.1",
Expand All @@ -211,53 +191,16 @@
"Newtonsoft.Json": "13.0.3"
}
},
"Microsoft.VisualStudio.Threading.Only": {
"type": "Transitive",
"resolved": "17.14.15",
"contentHash": "NqONyw1RXyj9P3k5e1uU2k9kc1ptwuU5NJQzG+MPq7vQVHUzBY8HLuJf/N2Rw5H/myD96CVxziDxmjawPuzntw==",
"dependencies": {
"Microsoft.VisualStudio.Validation": "17.8.8"
}
},
"Microsoft.VisualStudio.Validation": {
"type": "Transitive",
"resolved": "17.13.22",
"contentHash": "fC20ITOxlUpGQ0ltAxITQbeFxwuB6OjLT3lByC4+/Gm46o/Mwv9hlIVrBhiUhnqdQzUUvr4EHkdiYe7WOSMLmw=="
},
"Microsoft.Win32.SystemEvents": {
"type": "Transitive",
"resolved": "6.0.0",
"contentHash": "hqTM5628jSsQiv+HGpiq3WKBl2c8v1KZfby2J6Pr7pEPlK9waPdgEO6b8A/+/xn/yZ9ulv8HuqK71ONy2tg67A=="
},
"Nerdbank.MessagePack": {
"type": "Transitive",
"resolved": "1.0.2",
"contentHash": "mUcRO0trgc85qKPR360ybSvZTbm3nlSr1aK0JlKVWC5iC9OYxVLL1AbJxaW2Pbj4EQNCP8dKiz/SIlFPy67Ixw==",
"dependencies": {
"Microsoft.NET.StringTools": "18.0.2",
"Microsoft.VisualStudio.Validation": "17.13.22",
"PolyType": "1.0.0"
}
},
"Nerdbank.Streams": {
"type": "Transitive",
"resolved": "2.13.16",
"contentHash": "GjifQ5M4IpQWjGNNbIrsj8M/M7ESb2328OeoGeqxk5rOJiuaAJ5zFc6CyqB+5UWKr1KEGK23hKoxA+z1gooAKA==",
"dependencies": {
"Microsoft.VisualStudio.Threading.Only": "17.13.61",
"Microsoft.VisualStudio.Validation": "17.8.8"
}
},
"Newtonsoft.Json": {
"type": "Transitive",
"resolved": "13.0.3",
"contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ=="
},
"PolyType": {
"type": "Transitive",
"resolved": "1.0.0",
"contentHash": "q8vTQfDoKa/vCNMT+TvKbzsl+Z/s4k/qaP8WQGvNDIILkxKhP+N3NbFAOiglQljNmAj/ei51wO9jwcAI9jhtHw=="
},
"QRCoder": {
"type": "Transitive",
"resolved": "1.7.0",
Expand Down Expand Up @@ -308,19 +251,6 @@
"Spectre.Console": "0.49.1"
}
},
"StreamJsonRpc": {
"type": "Transitive",
"resolved": "2.24.84",
"contentHash": "7PtQlA7wYFBzOrcuALWGs1uPJI712KEYOMZXxxSKel9C0y5TPyjxXLkpw7d4O175rUKNbLHHbB89fPzMC2SLPg==",
"dependencies": {
"MessagePack": "2.5.198",
"Microsoft.VisualStudio.Threading.Only": "17.14.15",
"Microsoft.VisualStudio.Validation": "17.13.22",
"Nerdbank.MessagePack": "1.0.2",
"Nerdbank.Streams": "2.13.16",
"Newtonsoft.Json": "13.0.3"
}
},
"System.CommandLine": {
"type": "Transitive",
"resolved": "2.0.0-beta4.22272.1",
Expand Down Expand Up @@ -377,7 +307,7 @@
"coralph": {
"type": "Project",
"dependencies": {
"GitHub.Copilot.SDK": "[0.3.0, )",
"GitHub.Copilot.SDK": "[1.0.0, )",
"Hex1b": "[0.83.0, )",
"Microsoft.Extensions.Configuration.Json": "[8.0.0, )",
"Microsoft.Extensions.Options.ConfigurationExtensions": "[8.0.0, )",
Expand Down
Loading