-
Notifications
You must be signed in to change notification settings - Fork 0
feat: multi-agent communication via async tool replies #42
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
15 commits
Select commit
Hold shift + click to select a range
d1cc8fa
feat(multi-agent): async tool replies via signed callbacks + reply ro…
tsuz 8fa31e2
docs(example): add multi-agent-setup example for async tool delegation
tsuz e32d3ae
fix(build): skip test compile in image builds; note reply-to keying TODO
tsuz 3d4d447
fix(example): build think-consumer from source to match ThinkResponse…
tsuz 325932b
fix(processing): read both think-consumer ThinkResponse wire schemas
tsuz ffee6b3
test(example): add end-to-end integration test for multi-agent-setup
tsuz 7c6ca74
ci: add multi-agent integration workflow (first integration test)
tsuz dbd6a45
ci: make multi-agent integration a manual, gating PR check
tsuz 49dc0d3
ci: run multi-agent integration post-merge on main
tsuz 2290596
ci: also run multi-agent integration on api/ and frontend/ changes
tsuz b6010f7
use correct endpoint
tsuz 3064204
use correct endpoint
tsuz d2740b4
fix(processing): don't stamp session_id into user_id in result-before…
tsuz a9050f0
fix(multi-agent): remove SSRF primitive from reply-to callback routing
tsuz d011c40
fix(api): honor interruption during reply-delivery HTTP send
tsuz File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,53 @@ | ||
| name: Multi-Agent Integration | ||
|
|
||
| # End-to-end test of the examples/multi-agent-setup stack: build the images, | ||
| # send a message to orchestrator-api, and verify the orchestrator → worker → | ||
| # callback round-trip. | ||
| # | ||
| # Runs post-merge on main (and on manual dispatch) — not as a PR gate. It hits | ||
| # the real Claude API, so it needs the CLAUDE_API_KEY repo secret; if the secret | ||
| # is unset the job soft-skips rather than failing main. | ||
|
|
||
| on: | ||
| push: | ||
| branches: [main] | ||
| paths: | ||
| - examples/multi-agent-setup/** | ||
| - api/** | ||
| - frontend/** | ||
| - processor-apps/processing/** | ||
| - think/think-consumer/** | ||
| - .github/workflows/multi-agent-integration.yml | ||
| workflow_dispatch: | ||
|
|
||
| jobs: | ||
| e2e: | ||
| name: Orchestrator → Worker round-trip | ||
| runs-on: ubuntu-latest | ||
| timeout-minutes: 30 | ||
|
|
||
| steps: | ||
| - uses: actions/checkout@v4 | ||
|
|
||
| - name: Check for CLAUDE_API_KEY secret | ||
| id: guard | ||
| env: | ||
| CLAUDE_API_KEY: ${{ secrets.CLAUDE_API_KEY }} | ||
| run: | | ||
| if [ -z "$CLAUDE_API_KEY" ]; then | ||
| echo "::warning::CLAUDE_API_KEY secret not set — skipping integration test" | ||
| echo "skip=true" >> "$GITHUB_OUTPUT" | ||
| fi | ||
|
|
||
| - name: Run integration test | ||
| if: steps.guard.outputs.skip != 'true' | ||
| working-directory: examples/multi-agent-setup | ||
| env: | ||
| CLAUDE_API_KEY: ${{ secrets.CLAUDE_API_KEY }} | ||
| TIMEOUT: "300" | ||
| run: ./integration-test.sh | ||
|
|
||
| - name: Tear down (backstop) | ||
| if: always() | ||
| working-directory: examples/multi-agent-setup | ||
| run: docker compose -p multiagent-it down -v || true |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
81 changes: 81 additions & 0 deletions
81
api/chat-api/src/main/java/io/flightdeck/api/CallbackRegistry.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,81 @@ | ||
| package io.flightdeck.api; | ||
|
|
||
| import java.util.Collections; | ||
| import java.util.LinkedHashMap; | ||
| import java.util.Map; | ||
|
|
||
| /** | ||
| * Resolves a logical callback-service name to the trusted base URL configured for | ||
| * it, then appends the fixed callback path ({@value #CALLBACK_PATH}). | ||
| * | ||
| * <p>The mapping is loaded once from the {@code ALLOWED_HOST_MAPPING} environment | ||
| * variable: a comma-separated list of {@code name:baseUrl} entries. Only the first | ||
| * colon of each entry separates the name from the URL, so the base URL keeps its | ||
| * own {@code scheme://host[:port]} colons: | ||
| * | ||
| * <pre> | ||
| * ALLOWED_HOST_MAPPING=my-agent-a:https://hosta.local,my-agent-c:http://hostc.local | ||
| * </pre> | ||
| * | ||
| * <p><b>Why this exists.</b> A caller (a peer agent) supplies only the service | ||
| * <em>name</em> in its {@code reply} descriptor; the destination URL is chosen | ||
| * here from operator-controlled config, never from caller input. An untrusted | ||
| * caller therefore cannot steer the server-side callback at an arbitrary host — | ||
| * the SSRF primitive that an attacker-controlled {@code endpoint} would create is | ||
| * structurally removed. Unknown names fail closed. | ||
| */ | ||
| final class CallbackRegistry { | ||
|
|
||
| /** Fixed path appended to every resolved base URL. */ | ||
| static final String CALLBACK_PATH = "/api/tools/response"; | ||
|
|
||
| private static final Map<String, String> MAPPING = | ||
| parse(ChatApiApp.env("ALLOWED_HOST_MAPPING", "")); | ||
|
|
||
| private CallbackRegistry() {} | ||
|
|
||
| /** True if {@code service} resolves to a configured base URL. */ | ||
| static boolean isKnown(String service) { | ||
| return service != null && MAPPING.containsKey(service); | ||
| } | ||
|
|
||
| /** | ||
| * Resolves the full callback URL for a service name. | ||
| * | ||
| * @throws IllegalArgumentException if the name is not configured (fail closed) | ||
| */ | ||
| static String resolve(String service) { | ||
| String base = service == null ? null : MAPPING.get(service); | ||
| if (base == null) { | ||
| throw new IllegalArgumentException("unknown callbackService: " + service); | ||
| } | ||
| return toCallbackUrl(base); | ||
| } | ||
|
|
||
| /** Strips any trailing slash from the base URL and appends the fixed callback path. */ | ||
| static String toCallbackUrl(String base) { | ||
| String trimmed = base.endsWith("/") ? base.substring(0, base.length() - 1) : base; | ||
| return trimmed + CALLBACK_PATH; | ||
| } | ||
|
|
||
| /** Parses {@code name:baseUrl} entries, splitting each on its FIRST colon only. */ | ||
| static Map<String, String> parse(String raw) { | ||
| Map<String, String> mapping = new LinkedHashMap<>(); | ||
| if (raw == null || raw.isBlank()) { | ||
| return Collections.unmodifiableMap(mapping); | ||
| } | ||
| for (String entry : raw.split(",")) { | ||
| String e = entry.trim(); | ||
| if (e.isEmpty()) continue; | ||
| int sep = e.indexOf(':'); | ||
| String name = sep > 0 ? e.substring(0, sep).trim() : ""; | ||
| String url = sep > 0 ? e.substring(sep + 1).trim() : ""; | ||
| if (name.isEmpty() || url.isEmpty()) { | ||
| throw new IllegalArgumentException( | ||
| "Malformed ALLOWED_HOST_MAPPING entry (expected name:baseUrl): " + entry); | ||
| } | ||
| mapping.put(name, url); | ||
| } | ||
| return Collections.unmodifiableMap(mapping); | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.