Skip to content

Commit 0741f6d

Browse files
committed
Merge origin/main into feat/mcp-client-integration
2 parents fe0cfd8 + a37bcf4 commit 0741f6d

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

70 files changed

+2145
-1198
lines changed

.github/workflows/ci.yml

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
branches: [main]
8+
9+
env:
10+
CARGO_TERM_COLOR: always
11+
RUSTFLAGS: "-Dwarnings"
12+
13+
jobs:
14+
check:
15+
name: Check & Clippy
16+
runs-on: ubuntu-24.04
17+
steps:
18+
- uses: actions/checkout@v4
19+
20+
- name: Install protoc
21+
run: sudo apt-get update && sudo apt-get install -y protobuf-compiler
22+
23+
- uses: dtolnay/rust-toolchain@stable
24+
with:
25+
components: clippy
26+
27+
- uses: Swatinem/rust-cache@v2
28+
29+
- name: cargo check
30+
run: cargo check --all-targets
31+
32+
- name: cargo clippy
33+
run: cargo clippy --all-targets
34+
35+
fmt:
36+
name: Format
37+
runs-on: ubuntu-24.04
38+
steps:
39+
- uses: actions/checkout@v4
40+
41+
- uses: dtolnay/rust-toolchain@stable
42+
with:
43+
components: rustfmt
44+
45+
- name: cargo fmt
46+
run: cargo fmt --all -- --check
47+
48+
test:
49+
name: Test
50+
runs-on: ubuntu-24.04
51+
steps:
52+
- uses: actions/checkout@v4
53+
54+
- name: Install protoc
55+
run: sudo apt-get update && sudo apt-get install -y protobuf-compiler
56+
57+
- uses: dtolnay/rust-toolchain@stable
58+
59+
- uses: Swatinem/rust-cache@v2
60+
61+
- name: cargo test (lib)
62+
run: cargo test --lib

AGENTS.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -341,6 +341,8 @@ Phase 6 — Hardening:
341341

342342
**Don't abbreviate variable names.** `queue` not `q`, `message` not `msg`, `channel` not `ch`. Common abbreviations like `config` are fine.
343343

344+
**Don't add new features without updating existing docs.** When a feature change affects user-facing configuration, behaviour, or architecture, update the relevant existing documentation (`README.md`, `docs/`) in the same commit or PR. Don't create new doc files for this — update what's already there.
345+
344346
## Patterns to Implement
345347

346348
These are validated patterns from research (see `docs/research/pattern-analysis.md`). Implement them when building the relevant module.

Cargo.toml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -143,9 +143,9 @@ urlencoding = "2.1.3"
143143
metrics = ["dep:prometheus"]
144144

145145
[lints.clippy]
146-
dbg_macro = "forbid"
147-
todo = "forbid"
148-
unimplemented = "forbid"
146+
dbg_macro = "deny"
147+
todo = "deny"
148+
unimplemented = "deny"
149149

150150
[dev-dependencies]
151151
tokio-test = "0.4"
Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
# Named Messaging Adapters via Bindings
2+
3+
## Context
4+
5+
Messaging adapters are currently singleton per platform per instance (one Discord bot token, one Telegram bot token, one Slack app pair). Bindings route traffic by platform + location filters (guild/workspace/chat/channel), but they do not select which credential instance handles that traffic.
6+
7+
This blocks multi-bot scenarios on a single instance, such as:
8+
9+
- Two Telegram bots routing to different agents
10+
- Two Discord bots with different server memberships and identities
11+
- A default shared bot plus one specialized bot for a specific agent
12+
13+
The existing binding abstraction is still the right place to express routing. The missing piece is adapter instance selection.
14+
15+
## Proposal
16+
17+
Add support for multiple named adapter instances per platform, while keeping the existing nameless token fields as the default instance for backward compatibility.
18+
19+
- Existing config shape remains valid
20+
- Named instances are optional and additive
21+
- Bindings gain an optional `adapter` field
22+
- Binding resolution chooses both agent and adapter instance
23+
24+
This keeps the simple path simple (paste one token) and unlocks advanced routing when needed.
25+
26+
## Goals
27+
28+
- Support multiple credential instances per messaging platform
29+
- Keep old configs and API payloads working unchanged
30+
- Keep bindings as the single routing abstraction
31+
- Preserve first-match binding behavior
32+
- Avoid introducing a separate per-agent messaging config system
33+
34+
## Non-Goals
35+
36+
- No backward-compat shim for legacy behavior beyond config/API compatibility
37+
- No cross-platform global adapter namespace
38+
- No automatic migration that rewrites user config files
39+
40+
## Config Shape
41+
42+
### Telegram example
43+
44+
```toml
45+
[messaging.telegram]
46+
enabled = true
47+
token = "env:TELEGRAM_BOT_TOKEN" # default instance (legacy path)
48+
49+
[[messaging.telegram.instances]]
50+
name = "support"
51+
enabled = true
52+
token = "env:TELEGRAM_BOT_TOKEN_SUPPORT"
53+
54+
[[messaging.telegram.instances]]
55+
name = "sales"
56+
enabled = true
57+
token = "env:TELEGRAM_BOT_TOKEN_SALES"
58+
59+
[[bindings]]
60+
agent_id = "main"
61+
channel = "telegram"
62+
chat_id = "-100111111111"
63+
# adapter omitted => default instance
64+
65+
[[bindings]]
66+
agent_id = "support-agent"
67+
channel = "telegram"
68+
chat_id = "-100222222222"
69+
adapter = "support"
70+
```
71+
72+
### Binding semantics
73+
74+
- `adapter` is platform-scoped (same name can exist under Telegram and Discord)
75+
- `adapter` omitted means default instance for that platform
76+
- If no matching binding exists, message still routes to default agent (current behavior)
77+
78+
## Data Model Changes
79+
80+
### `Binding`
81+
82+
Add:
83+
84+
- `adapter: Option<String>`
85+
86+
### Messaging config structs
87+
88+
For each platform with token-based auth (Discord, Telegram, Slack, Twitch):
89+
90+
- Keep existing singleton fields (`token`, `bot_token`, `app_token`, etc.)
91+
- Add optional `instances: Vec<...InstanceConfig>` with:
92+
- `name: String`
93+
- `enabled: bool`
94+
- platform credential fields
95+
- optional platform-specific extras if needed later
96+
97+
## Runtime Model
98+
99+
## Adapter identity
100+
101+
Each running adapter gets a stable runtime key:
102+
103+
- Default instance: `<platform>` (example: `telegram`)
104+
- Named instance: `<platform>:<name>` (example: `telegram:support`)
105+
106+
`MessagingManager` stores adapters by runtime key instead of platform name alone.
107+
108+
## Inbound routing
109+
110+
When an adapter emits an inbound message:
111+
112+
- Keep `message.source = <platform>` for existing platform semantics
113+
- Add `adapter` metadata (or equivalent field) carrying runtime adapter key
114+
115+
Binding resolution matches on:
116+
117+
1. platform (`channel`)
118+
2. adapter selector (`binding.adapter` vs inbound adapter identity)
119+
3. existing platform filters (`guild_id`, `workspace_id`, `chat_id`, `channel_ids`, DMs)
120+
121+
Then first-match wins as before.
122+
123+
## Outbound routing
124+
125+
`respond`, `send_status`, and `fetch_history` use the adapter identity captured on inbound so replies stay on the same bot instance.
126+
127+
For proactive sends (`broadcast` and tools), the caller can target runtime adapter key explicitly when required.
128+
129+
## Permissions Model
130+
131+
Permission maps currently aggregate per platform. They become per adapter instance:
132+
133+
- Build permission set by filtering bindings on `(platform, adapter)`
134+
- Default adapter uses bindings where `adapter` is absent or explicitly set to default selector
135+
- Named adapter uses bindings with matching `adapter`
136+
137+
This allows independent scope per token instance without changing binding filter fields.
138+
139+
## API Changes
140+
141+
## Bindings API
142+
143+
`POST/PUT/DELETE /bindings` payloads add optional:
144+
145+
- `adapter?: string`
146+
147+
Old payloads remain valid.
148+
149+
## Messaging API
150+
151+
Status/toggle/disconnect move from platform-only targeting to adapter instance targeting:
152+
153+
- Platform + optional adapter name
154+
- Platform-only continues to refer to default instance for compatibility
155+
156+
Response payloads should include adapter instance identity so UI can render multiple cards per platform.
157+
158+
## UI Changes
159+
160+
- Keep current quick setup flow for default token
161+
- Add “Add another token” flow that requires a name
162+
- Settings display becomes list of adapter instances per platform
163+
- Binding editor adds optional adapter selector (default preselected)
164+
165+
The common single-token path remains one step.
166+
167+
## Validation Rules
168+
169+
- No duplicate instance names within a platform
170+
- Binding `adapter` must reference an existing configured instance for that platform
171+
- Reserved names: reject empty names and `default`
172+
- Runtime key collisions are impossible under platform-scoped names but still validated
173+
174+
Config load should fail fast with clear messages when these constraints are violated.
175+
176+
## Backward Compatibility
177+
178+
- Existing `[messaging.<platform>]` blocks continue to create the default adapter
179+
- Existing bindings with no `adapter` continue to work unchanged
180+
- Existing API clients can omit `adapter`
181+
- Existing docs/examples remain valid; new docs add advanced multi-instance examples
182+
183+
No migration required for existing users.
184+
185+
## Failure Modes and Handling
186+
187+
- Binding references missing adapter: reject config/API mutation
188+
- Named adapter disabled/disconnected: bindings remain but produce clear routing/health errors
189+
- Duplicate adapter name: reject config load and UI mutation
190+
- Default adapter missing while bindings rely on default: validation error
191+
192+
## Ordered Implementation Phases
193+
194+
### Phase 1: Config and binding model
195+
196+
1. Add `adapter` to binding structs and TOML parsing
197+
2. Add per-platform `instances` config parsing
198+
3. Add validation rules (names, existence, duplicates)
199+
4. Update docs for config reference
200+
201+
### Phase 2: Runtime adapter identity
202+
203+
1. Refactor `MessagingManager` keying from platform name to runtime adapter key
204+
2. Instantiate default + named adapters per platform
205+
3. Attach adapter identity to inbound messages
206+
4. Route outbound operations via inbound adapter identity
207+
208+
### Phase 3: Binding resolution and permissions
209+
210+
1. Extend binding match logic with adapter selector
211+
2. Build per-adapter permission sets from filtered bindings
212+
3. Ensure hot-reload updates per-adapter permissions correctly
213+
214+
### Phase 4: API and UI
215+
216+
1. Extend bindings API payloads with optional `adapter`
217+
2. Extend messaging status/toggle/disconnect APIs for adapter instances
218+
3. Update dashboard settings and binding editor for named instances
219+
4. Preserve platform-only behavior for default adapter paths
220+
221+
### Phase 5: Test coverage and rollout docs
222+
223+
1. Add config parsing/validation tests for named instances
224+
2. Add routing tests for adapter-specific bindings
225+
3. Add API tests for backward compatible payloads
226+
4. Add setup docs for multi-bot per platform scenarios
227+
228+
## Open Questions
229+
230+
- Should adapter identity be surfaced as a first-class field on `InboundMessage` instead of metadata?
231+
- For Slack, should named instances support independent app-level settings beyond tokens in this phase?
232+
- Should proactive broadcast endpoints require explicit adapter for platforms with multiple configured instances, or keep default fallback?

interface/src/api/client.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -462,6 +462,7 @@ export interface RoutingSection {
462462
worker: string;
463463
compactor: string;
464464
cortex: string;
465+
voice: string;
465466
rate_limit_cooldown_secs: number;
466467
channel_thinking_effort: string;
467468
branch_thinking_effort: string;
@@ -537,6 +538,7 @@ export interface RoutingUpdate {
537538
worker?: string;
538539
compactor?: string;
539540
cortex?: string;
541+
voice?: string;
540542
rate_limit_cooldown_secs?: number;
541543
channel_thinking_effort?: string;
542544
branch_thinking_effort?: string;
@@ -702,6 +704,7 @@ export interface ModelInfo {
702704
context_window: number | null;
703705
tool_call: boolean;
704706
reasoning: boolean;
707+
input_audio: boolean;
705708
}
706709

707710
export interface ModelsResponse {
@@ -1158,8 +1161,11 @@ export const api = {
11581161
},
11591162

11601163
// Model listing
1161-
models: (provider?: string) => {
1162-
const query = provider ? `?provider=${encodeURIComponent(provider)}` : "";
1164+
models: (provider?: string, capability?: "input_audio" | "voice_transcription") => {
1165+
const params = new URLSearchParams();
1166+
if (provider) params.set("provider", provider);
1167+
if (capability) params.set("capability", capability);
1168+
const query = params.toString() ? `?${params.toString()}` : "";
11631169
return fetchJson<ModelsResponse>(`/models${query}`);
11641170
},
11651171
refreshModels: async () => {

interface/src/components/ModelSelect.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ interface ModelSelectProps {
99
value: string;
1010
onChange: (value: string) => void;
1111
provider?: string;
12+
capability?: "input_audio" | "voice_transcription";
1213
}
1314

1415
const PROVIDER_LABELS: Record<string, string> = {
@@ -18,6 +19,7 @@ const PROVIDER_LABELS: Record<string, string> = {
1819
deepseek: "DeepSeek",
1920
xai: "xAI",
2021
mistral: "Mistral",
22+
gemini: "Google Gemini",
2123
groq: "Groq",
2224
together: "Together AI",
2325
fireworks: "Fireworks AI",
@@ -38,15 +40,16 @@ export function ModelSelect({
3840
value,
3941
onChange,
4042
provider,
43+
capability,
4144
}: ModelSelectProps) {
4245
const [open, setOpen] = useState(false);
4346
const [filter, setFilter] = useState("");
4447
const containerRef = useRef<HTMLDivElement>(null);
4548
const inputRef = useRef<HTMLInputElement>(null);
4649

4750
const { data } = useQuery({
48-
queryKey: ["models", provider ?? "configured"],
49-
queryFn: () => api.models(provider),
51+
queryKey: ["models", provider ?? "configured", capability ?? "all"],
52+
queryFn: () => api.models(provider, capability),
5053
staleTime: 60_000,
5154
});
5255

@@ -128,6 +131,7 @@ export function ModelSelect({
128131
"deepseek",
129132
"xai",
130133
"mistral",
134+
"gemini",
131135
"groq",
132136
"together",
133137
"fireworks",

0 commit comments

Comments
 (0)