Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
35704a9
feat(channels): add ChannelIntegrationConfiguration resource + store …
ginccc Apr 18, 2026
53e8224
feat(channels): add ChannelTargetRouter with colon-required trigger m…
ginccc Apr 18, 2026
9f2b330
docs: add conversation cancel & lifecycle control plan
ginccc Apr 18, 2026
a69d5fa
refactor(slack): replace SlackChannelRouter with ChannelTargetRouter
ginccc Apr 18, 2026
5175926
feat(channels): add MCP admin tools + migration helper for channel in…
ginccc Apr 18, 2026
ed684db
docs: update changelog and task list for channel integration work
ginccc Apr 18, 2026
cebdcde
fix(channels): fix 4 bugs found in code review
ginccc Apr 18, 2026
e4e493a
fix(channels): address code review findings #1-12
ginccc Apr 18, 2026
a45bee1
docs(channels): add code review hardening changelog entry
ginccc Apr 18, 2026
39a37a8
fix(channels): harden migration tool β€” N1-N7 from re-review
ginccc Apr 18, 2026
63050b8
docs(channels): add migration tool hardening changelog entry (N1-N7)
ginccc Apr 18, 2026
6abb1f7
test(channels): add 19 tests + polish migration edge cases
ginccc Apr 18, 2026
1925a01
refactor(channels): replace MCP migration with startup migration, dep…
ginccc Apr 18, 2026
1200ebd
docs(channels): add startup migration & deprecation changelog entry
ginccc Apr 18, 2026
aa0d1bf
fix(channels): readAgentβ†’read on IAgentStore, signing secret from res…
ginccc Apr 18, 2026
c861cb0
test(channels): add 31 tests for refresh, public API, secrets, legacy…
ginccc Apr 18, 2026
ded7110
docs(changelog): review hardening and test coverage entry
ginccc Apr 18, 2026
aa759f0
chore(channels): remove unused IResourceStore import
ginccc Apr 18, 2026
4733411
fix(channels): address review bugs β€” legacy posting, duplicate channe…
ginccc Apr 19, 2026
2e3c831
docs(changelog): external review round 4 β€” 6 bugs fixed, 80 tests
ginccc Apr 19, 2026
2121f18
test(channels): comprehensive test coverage for channel integration s…
ginccc Apr 24, 2026
9d44453
chore(merge): resolve merge conflicts from main into feature/channel-…
ginccc Apr 25, 2026
0537232
fix(channels): reject duplicate target names in validation
ginccc Apr 25, 2026
dd9a6fd
fix(channels): address PR review β€” defensive copies, migration retry,…
ginccc Apr 25, 2026
67ab9ba
fix(channels): fix duplicateChannel regression from defensive copying
ginccc Apr 25, 2026
95f2759
fix(ci): harden Postgres integration tests against CI flakiness
ginccc Apr 25, 2026
94d6d0f
fix(migration): inject IMigrationLogStore interface instead of concre…
ginccc Apr 26, 2026
fed27e7
fix(channels): address review findings β€” idempotency, intent keys, he…
ginccc Apr 26, 2026
4fa03f9
Merge remote-tracking branch 'origin/main' into feature/channel-integ…
ginccc Apr 26, 2026
ead310d
Merge remote-tracking branch 'origin/main' into feature/channel-integ…
ginccc May 14, 2026
4c209b2
fix(channels): address code review β€” ThreadLocal removal, vault prefi…
ginccc May 14, 2026
dc7b570
fix(channels): second-pass review β€” log sanitization, defensive copie…
ginccc May 14, 2026
65ec272
docs(changelog): add second-pass review fixes entry
ginccc May 14, 2026
7711817
Merge remote-tracking branch 'origin/main' into feature/channel-integ…
ginccc May 17, 2026
f6ded96
refactor(slack): remove eddi.slack.enabled server toggle β€” config-dri…
ginccc May 17, 2026
50fe538
fix(slack): sanitize attacker-controlled headers in log statements
ginccc May 17, 2026
31ca10a
feat(slack): DM support, test hardening, docs overhaul
ginccc May 18, 2026
bf7a94c
fix(llm): update LlmTaskTest template counts for TEMPLATE_SKIP_PARAMS
ginccc May 18, 2026
86b0694
fix(slack): close table fence before real code block in mrkdwn converter
ginccc May 18, 2026
a01e4f7
chore: merge origin/main into feature/channel-integrations
ginccc May 20, 2026
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
301 changes: 301 additions & 0 deletions docs/changelog.md

Large diffs are not rendered by default.

40 changes: 40 additions & 0 deletions docs/group-conversations.md
Original file line number Diff line number Diff line change
Expand Up @@ -205,9 +205,49 @@ For full control, define phases directly:
| `read_group_conversation` | Read conversation transcript |
| `list_group_conversations` | List past discussions |

## Slack Integration

Group discussions integrate natively with Slack. See [slack-integration.md](slack-integration.md) for full setup instructions.

### UX Pattern: Header + Thread

All discussion styles use the same rendering pattern in Slack:

1. **Start Banner** β€” posted in the user's thread with style name, agent count, and question
2. **Agent Headers** β€” each agent's first contribution is a channel-level message with a short preview
3. **Full Content** β€” the complete response is posted as a thread reply under the agent's header
4. **Peer Feedback** β€” feedback threads under the target agent's header message
5. **Revisions** β€” revised contributions thread under the agent's own header
6. **Synthesis** β€” moderator's synthesis gets its own channel-level header + thread

### Discussion Styles in Slack

| Style | Phase Flow in Slack |
|-------|-------------------|
| **ROUND_TABLE** | Each agent posts β†’ Moderator synthesizes |
| **PEER_REVIEW** | Agents post β†’ Critiques thread under targets β†’ Revisions thread under own β†’ Synthesis |
| **DEVIL_ADVOCATE** | Agent posts β†’ Challenger threads challenges β†’ Agent threads defense β†’ Synthesis |
| **DEBATE** | PRO agent posts β†’ CON agent posts β†’ Rebuttals thread under opponents β†’ Judge synthesizes |
| **DELPHI** | Round 1 agents post β†’ Round 2 agents post (convergence) β†’ Synthesis |

### Trigger Keywords

Configure trigger keywords in `ChannelIntegrationConfiguration` to route to specific groups:

```
@EDDI panel: Should we adopt microservices? β†’ GROUP target "panel"
@EDDI debate: REST vs GraphQL β†’ GROUP target "debate"
@EDDI peer: Review this architecture β†’ GROUP target "peer"
```

### Follow-up Conversations

After a discussion, users can reply in any agent's thread to ask follow-up questions. The system injects the agent's discussion context (contribution + peer feedback received) into the prompt for a contextual response.

## Configuration

```properties
# application.properties
eddi.groups.max-depth=3 # Max recursion depth for nested groups
```

400 changes: 242 additions & 158 deletions docs/slack-integration.md

Large diffs are not rendered by default.

374 changes: 374 additions & 0 deletions planning/conversation-cancel-plan.md

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -556,6 +556,10 @@
Prevents CI from hanging indefinitely when Docker image builds
or container startup fails to propagate errors. -->
<forkedProcessTimeoutInSeconds>900</forkedProcessTimeoutInSeconds>
<!-- Retry container-startup failures once. Testcontainers ITs
can flake on CI runners due to Docker networking timing.
This gives one automatic retry before failing the build. -->
<rerunFailingTestsCount>1</rerunFailingTestsCount>
<systemPropertyVariables>
<native.image.path>${project.build.directory}/${project.build.finalName}-runner
</native.image.path>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,14 @@
public class AgentConfiguration {
@JsonAlias("packages")
private List<URI> workflows = new ArrayList<>();
/**
* @deprecated Since 6.1.0. Use standalone
* {@code ChannelIntegrationConfiguration} documents instead. Legacy
* connectors are auto-migrated at startup by
* {@code ChannelConnectorMigration}. This field will be removed in
* a future release.
*/
@Deprecated(since = "6.1.0", forRemoval = true)
private List<ChannelConnector> channels = new ArrayList<>();

/**
Expand Down Expand Up @@ -78,6 +86,11 @@ public class AgentConfiguration {
*/
private SessionManagement sessionManagement;

/**
* @deprecated Since 6.1.0. Replaced by {@code ChannelIntegrationConfiguration}
* with multi-target routing support.
*/
@Deprecated(since = "6.1.0", forRemoval = true)
public static class ChannelConnector {
private URI type;
private Map<String, String> config = new HashMap<>();
Expand Down Expand Up @@ -107,10 +120,12 @@ public void setWorkflows(List<URI> workflows) {
this.workflows = workflows;
}

@Deprecated(since = "6.1.0", forRemoval = true)
public List<ChannelConnector> getChannels() {
return channels;
}

@Deprecated(since = "6.1.0", forRemoval = true)
public void setChannels(List<ChannelConnector> channels) {
this.channels = channels;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/*
* Copyright EDDI contributors
* SPDX-License-Identifier: Apache-2.0
*/
package ai.labs.eddi.configs.channels;

import ai.labs.eddi.configs.channels.model.ChannelIntegrationConfiguration;
import ai.labs.eddi.datastore.IResourceStore;

/**
* Store interface for channel integration configurations. Uses the DB-agnostic
* {@code AbstractResourceStore} via {@code IResourceStorageFactory}.
*
* @since 6.1.0
*/
public interface IChannelIntegrationStore extends IResourceStore<ChannelIntegrationConfiguration> {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
/*
* Copyright EDDI contributors
* SPDX-License-Identifier: Apache-2.0
*/
package ai.labs.eddi.configs.channels;

import ai.labs.eddi.configs.IRestVersionInfo;
import ai.labs.eddi.configs.channels.model.ChannelIntegrationConfiguration;
import ai.labs.eddi.configs.descriptors.model.DocumentDescriptor;
import jakarta.annotation.security.RolesAllowed;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import org.eclipse.microprofile.openapi.annotations.Operation;
import org.eclipse.microprofile.openapi.annotations.parameters.Parameter;
import org.eclipse.microprofile.openapi.annotations.tags.Tag;

import java.util.List;

/**
* JAX-RS interface for channel integration configuration CRUD.
* <p>
* Admin-only β€” channel configurations expose target topology and vault
* references. Same security posture as {@code IRestAgentStore}.
*
* @since 6.1.0
*/
@Path("/channelstore/channels")
@Tag(name = "Channel Integrations")
@RolesAllowed({"eddi-admin", "eddi-editor"})
public interface IRestChannelIntegrationStore extends IRestVersionInfo {
String resourceBaseType = "eddi://ai.labs.channel";
String resourceURI = resourceBaseType + "/channelstore/channels/";

@GET
@Path("/descriptors")
@Produces(MediaType.APPLICATION_JSON)
@Operation(description = "Read list of channel integration descriptors.")
List<DocumentDescriptor> readChannelDescriptors(
@QueryParam("filter")
@DefaultValue("") String filter,
@QueryParam("index")
@DefaultValue("0") Integer index,
@QueryParam("limit")
@DefaultValue("20") Integer limit);

@GET
@Path("/{id}")
@Produces(MediaType.APPLICATION_JSON)
@Operation(description = "Read channel integration configuration.")
ChannelIntegrationConfiguration readChannel(
@PathParam("id") String id,
@Parameter(name = "version", required = true, example = "1")
@QueryParam("version") Integer version);

@PUT
@Path("/{id}")
@Consumes(MediaType.APPLICATION_JSON)
@Operation(description = "Update channel integration configuration.")
Response updateChannel(
@PathParam("id") String id,
@Parameter(name = "version", required = true, example = "1")
@QueryParam("version") Integer version,
ChannelIntegrationConfiguration channelConfiguration);

@POST
@Consumes(MediaType.APPLICATION_JSON)
@Operation(description = "Create channel integration configuration.")
Response createChannel(ChannelIntegrationConfiguration channelConfiguration);

@POST
@Path("/{id}")
@Operation(description = "Duplicate this channel integration configuration.")
Response duplicateChannel(@PathParam("id") String id,
@QueryParam("version") Integer version);

@DELETE
@Path("/{id}")
@Operation(description = "Delete channel integration configuration.")
Response deleteChannel(
@PathParam("id") String id,
@Parameter(name = "version", required = true, example = "1")
@QueryParam("version") Integer version,
@QueryParam("permanent")
@DefaultValue("false") Boolean permanent);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/*
* Copyright EDDI contributors
* SPDX-License-Identifier: Apache-2.0
*/
package ai.labs.eddi.configs.channels.model;

import com.fasterxml.jackson.annotation.JsonInclude;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
* Standalone configuration for a channel integration. Decouples channel routing
* and credentials from {@code AgentConfiguration}, supporting multi-target
* channels and cross-platform extensibility (Slack, Teams, Discord).
* <p>
* One {@code ChannelIntegrationConfiguration} represents one platform channel
* (e.g., a single Slack channel) with one or more {@link ChannelTarget}s that
* map trigger keywords to agents or groups.
*
* @since 6.1.0
*/
@JsonInclude(JsonInclude.Include.NON_NULL)
public class ChannelIntegrationConfiguration {

private String name;
private String channelType;
private Map<String, String> platformConfig;
private List<ChannelTarget> targets;
private String defaultTargetName;

public ChannelIntegrationConfiguration() {
this.platformConfig = new HashMap<>();
this.targets = new ArrayList<>();
}

/**
* Human-readable name for this integration (e.g., "Engineering AI Hub").
*/
public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

/**
* Platform type. Validated server-side against registered adapters. Currently
* supported: {@code "slack"}. Future: {@code "teams"}, {@code "discord"}.
* <p>
* Deliberately a String (not an enum) so downstream forks can register custom
* adapters without recompiling core.
*/
public String getChannelType() {
return channelType;
}

public void setChannelType(String channelType) {
this.channelType = channelType;
}

/**
* Platform-specific credentials and identifiers. Keys depend on
* {@link #channelType}:
* <ul>
* <li><b>slack:</b> {@code channelId}, {@code botToken},
* {@code signingSecret}</li>
* <li><b>teams:</b> {@code channelId}, {@code appId}, {@code appPassword},
* {@code serviceUrl}</li>
* <li><b>discord:</b> {@code guildId}, {@code channelId}, {@code botToken},
* {@code publicKey}</li>
* </ul>
* Secret values should use vault references: {@code ${vault:key-name}}.
*/
public Map<String, String> getPlatformConfig() {
Comment thread
github-code-quality[bot] marked this conversation as resolved.
Fixed
return new HashMap<>(platformConfig);
}

public void setPlatformConfig(Map<String, String> platformConfig) {
this.platformConfig = platformConfig == null ? new HashMap<>() : new HashMap<>(platformConfig);
}

/**
* Available targets in this channel. Each target maps trigger keywords to an
* agent or group. At least one target is required.
*/
public List<ChannelTarget> getTargets() {
return new ArrayList<>(targets);
}

public void setTargets(List<ChannelTarget> targets) {
this.targets = targets == null ? new ArrayList<>() : new ArrayList<>(targets);
}

/**
* Name of the target to use when no trigger keyword matches. Must reference an
* existing target in {@link #targets}. Required.
*/
public String getDefaultTargetName() {
return defaultTargetName;
}

public void setDefaultTargetName(String defaultTargetName) {
this.defaultTargetName = defaultTargetName;
}
}
Loading
Loading