├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE │ ├── 00-bug.md │ ├── 01-feature.md │ ├── 02-question.md │ └── 03-proposal.md ├── codecov.yaml └── workflows │ ├── cla.yml │ └── prc.yml ├── .gitignore ├── .golangci.yml ├── .typos.toml ├── CHANGELOG.md ├── CODE-OF-CONDUCT.md ├── CONTRIBUTING.md ├── CONTRIBUTORS.md ├── LICENSE ├── README.md ├── auth ├── provider.go ├── provider_test.go ├── push_notification.go └── push_notification_test.go ├── client ├── client.go ├── client_test.go ├── options.go └── options_test.go ├── examples ├── auth │ ├── client │ │ └── main.go │ └── server │ │ └── main.go ├── basic │ ├── README.md │ ├── client │ │ └── main.go │ └── server │ │ └── main.go ├── go.mod ├── go.sum ├── jwks │ ├── README.md │ ├── client │ │ └── main.go │ └── server │ │ └── main.go ├── multi │ ├── README.md │ ├── cli │ │ └── main.go │ ├── creative │ │ ├── main.go │ │ └── test.sh │ ├── exchange │ │ ├── main.go │ │ └── test.sh │ ├── go.mod │ ├── go.sum │ ├── reimbursement │ │ ├── main.go │ │ └── test.sh │ └── root │ │ └── main.go ├── simple │ ├── client │ │ └── main.go │ └── server │ │ └── main.go └── streaming │ ├── client │ └── main.go │ └── server │ └── main.go ├── go.mod ├── go.sum ├── internal ├── jsonrpc │ ├── jsonrpc_test.go │ ├── request.go │ ├── request_test.go │ ├── response.go │ └── types.go └── sse │ ├── sse.go │ └── sse_test.go ├── log ├── log.go └── log_test.go ├── protocol ├── protocol.go ├── protocol_test.go ├── types.go └── types_test.go ├── server ├── options.go ├── options_test.go ├── server.go ├── server_handlers_test.go ├── server_test.go └── types.go ├── taskmanager ├── errors.go ├── interface.go ├── memory.go ├── memory_test.go ├── redis │ ├── README.md │ ├── example │ │ ├── README.md │ │ ├── client │ │ │ └── main.go │ │ └── server │ │ │ └── main.go │ ├── go.mod │ ├── go.sum │ ├── options.go │ ├── push_notification.go │ ├── redis.go │ └── redis_test.go └── task.go └── tests ├── e2e_auth_test.go └── e2e_test.go /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # CODEOWNERS file for a2a-go-github 2 | # Each line is a file pattern followed by one or more owners. 3 | 4 | # Default owners for everything in the repo (unless a later match takes precedence) 5 | * @trpc-group/trpc-go-a2a-maintainer 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/00-bug.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "🐛 Bug report" 3 | description: Create a report to help us improve 4 | title: '' 5 | labels: ["type/bug"] 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Environment (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Version [e.g. 22] 29 | - Go version: 30 | - tRPC-A2A-go version: 31 | 32 | **Additional context** 33 | Add any other context about the problem here. -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/01-feature.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "✨ Feature request" 3 | description: Suggest an idea for this project 4 | title: '' 5 | labels: ["type/feature"] 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/02-question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "❓ Question" 3 | description: Ask questions about trpc-a2a-go 4 | title: '' 5 | labels: ["type/question"] 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Question** 11 | 12 | Please ask your question here. 13 | 14 | **If the question is related to code, add the following details:** 15 | - Code snippet or link to the code 16 | - Log 17 | - Configuration 18 | 19 | **Additional context** 20 | Add any other context about the question here. -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/03-proposal.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "💡 Proposal" 3 | description: Suggest a proposal for this project 4 | title: '' 5 | labels: ["type/proposal"] 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## Proposal 11 | 12 | **Background** 13 | 14 | **Proposal** 15 | 16 | **Goals** 17 | 18 | **Non-Goals** 19 | 20 | **Open Questions** -------------------------------------------------------------------------------- /.github/codecov.yaml: -------------------------------------------------------------------------------- 1 | coverage: # https://docs.codecov.com/docs/codecovyml-reference#coverage 2 | precision: 5 3 | range: 4 | - 85.0 5 | - 90.0 6 | status: 7 | project: 8 | default: 9 | branches: 10 | - ^main$ 11 | target: 85.0 # the minimum coverage ratio that the commit must meet to be considered a success. 12 | threshold: 1% # allow the coverage to drop by X%, and posting a success status. -------------------------------------------------------------------------------- /.github/workflows/cla.yml: -------------------------------------------------------------------------------- 1 | name: "CLA Assistant" 2 | on: 3 | issue_comment: 4 | types: [created] 5 | pull_request_target: 6 | types: [opened, closed, synchronize, reopened] 7 | 8 | # explicitly configure permissions, in case your GITHUB_TOKEN workflow permissions are set to read-only in repository settings 9 | permissions: 10 | actions: write 11 | contents: write 12 | pull-requests: write 13 | statuses: write 14 | 15 | jobs: 16 | CLAAssistant: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - name: "CLA Assistant" 20 | if: (github.event.comment.body == 'recheck' || github.event.comment.body == 'I have read the CLA Document and I hereby sign the CLA') || github.event_name == 'pull_request_target' 21 | uses: contributor-assistant/github-action@v2.3.1 22 | env: 23 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 24 | PERSONAL_ACCESS_TOKEN: ${{ secrets.CLA_DATABASE_ACCESS_TOKEN }} 25 | with: 26 | remote-organization-name: trpc-group 27 | remote-repository-name: cla-database 28 | path-to-signatures: 'signatures/${{ github.event.repository.name }}-${{ github.repository_id }}/cla.json' 29 | path-to-document: 'https://github.com/trpc-group/cla-database/blob/main/Tencent-Contributor-License-Agreement.md' 30 | # branch should not be protected 31 | branch: 'main' -------------------------------------------------------------------------------- /.github/workflows/prc.yml: -------------------------------------------------------------------------------- 1 | name: Pull Request Check 2 | on: 3 | pull_request: 4 | push: 5 | workflow_dispatch: 6 | permissions: 7 | contents: read 8 | pull-requests: read # Use with `only-new-issues` option. 9 | jobs: 10 | build: 11 | name: build 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v3 15 | - uses: actions/setup-go@v4 16 | with: 17 | go-version: 1.21 18 | - name: Build 19 | run: go build -v ./... 20 | - name: Test 21 | run: go test -v -coverprofile=coverage.out ./... 22 | - name: Upload coverage reports to Codecov 23 | uses: codecov/codecov-action@v3 24 | with: 25 | files: coverage.out 26 | flags: unittests 27 | env: 28 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 29 | golangci: 30 | name: lint 31 | runs-on: ubuntu-latest 32 | steps: 33 | - uses: actions/setup-go@v4 34 | with: 35 | go-version: 1.21 36 | - uses: actions/checkout@v3 37 | - name: golangci-lint 38 | uses: golangci/golangci-lint-action@v3 39 | with: 40 | version: latest 41 | only-new-issues: true 42 | args: --skip-files=.*_test.go 43 | typos: 44 | name: typos 45 | runs-on: ubuntu-latest 46 | steps: 47 | - uses: actions/checkout@v3 48 | - name: typos 49 | uses: crate-ci/typos@master 50 | go-apidiff: 51 | if: github.event_name == 'pull_request' 52 | runs-on: ubuntu-latest 53 | steps: 54 | - uses: actions/checkout@v3 55 | with: 56 | fetch-depth: 0 57 | - uses: actions/setup-go@v4 58 | with: 59 | go-version: 1.21 60 | - uses: joelanford/go-apidiff@main -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore files without extension 2 | * 3 | !*/ 4 | !*.* 5 | 6 | test_output.txt 7 | *cover.out* 8 | *.out 9 | 10 | *jwt-secret.key* 11 | 12 | !CODEOWNERS 13 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | linters-settings: 2 | gocyclo: 3 | min-complexity: 20 4 | goliddjoijnt: 5 | min-confidence: 0 6 | revive: 7 | rules: 8 | - name: package-comments 9 | - name: exported 10 | arguments: 11 | - disableStutteringCheck 12 | 13 | issues: 14 | include: 15 | - EXC0012 # exported should have comment 16 | - EXC0013 # package comment should be of the form 17 | - EXC0014 # comment on exported should be of the form 18 | - EXC0015 # should have a package comment 19 | 20 | linters: 21 | disable-all: true 22 | enable: 23 | - govet 24 | - goimports 25 | - gofmt 26 | - revive 27 | - gocyclo 28 | - gosec 29 | - ineffassign 30 | 31 | run: 32 | skip-files: 33 | - ".*.pb.go" 34 | - ".*_mock.go" -------------------------------------------------------------------------------- /.typos.toml: -------------------------------------------------------------------------------- 1 | # Typos check: https://github.com/crate-ci/typos 2 | 3 | # Ignore specific file types 4 | [type.csr] 5 | extend-glob = ["*.csr"] 6 | check-file = false 7 | 8 | # Ignore specific files and patterns 9 | [files] 10 | extend-exclude = [ 11 | "LICENSE", 12 | "*.sum", 13 | "go.sum", 14 | "*.trpc.go", 15 | "*.pb.go", 16 | "*_string.go", 17 | "*.gen.go", 18 | "examples/" 19 | ] 20 | 21 | # Allow specific identifiers 22 | [default.extend-identifiers] 23 | O_WRONLY = "O_WRONLY" 24 | 25 | # Allow specific words 26 | [default.extend-words] 27 | unmarshaling = "unmarshaling" -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 0.0.3 (2025-05-21) 4 | 5 | - Add `GetSessionId` to `TaskHandle` (#27) 6 | 7 | ## 0.0.2 (2025-04-18) 8 | 9 | - Change agent card provider `name` to `organization` 10 | 11 | ## 0.0.1 (2025-04-18) 12 | 13 | - Initial release 14 | 15 | ### Features 16 | 17 | - Implemented A2A protocol core components: 18 | - Complete type system with JSON-RPC message structures. 19 | - Client implementation for interacting with A2A servers. 20 | - Server implementation with HTTP endpoints handler. 21 | - In-memory task manager for task lifecycle management. 22 | - Redis-based task manager for persistent storage. 23 | - Flexible authentication system with JWT and API key support. 24 | 25 | ### Client Features 26 | 27 | - Agent discovery capabilities. 28 | - Task management (send, get status, cancel). 29 | - Streaming updates subscription. 30 | - Push notification configuration. 31 | - Authentication support for secure connections. 32 | 33 | ### Server Features 34 | 35 | - HTTP endpoint handlers for A2A protocol. 36 | - Request validation and routing. 37 | - Streaming response support. 38 | - Agent card configuration for capability discovery. 39 | - CORS support for cross-origin requests. 40 | - Authentication middleware integration. 41 | 42 | ### Task Management 43 | 44 | - Task lifecycle management. 45 | - Status transition tracking. 46 | - Resource management for running tasks. 47 | - Memory-based implementation for development. 48 | - Redis-based implementation for production use. 49 | 50 | ### Authentication 51 | 52 | - Multiple authentication scheme support. 53 | - JWT authentication with JWKS endpoint. 54 | - API key authentication. 55 | - OAuth2 integration. 56 | - Chain authentication for multiple auth methods. 57 | 58 | ### Examples 59 | 60 | - Basic text processing agent example. 61 | - Interactive CLI client. 62 | - Streaming data client sample. 63 | - Authentication server demonstration. 64 | - Redis task management implementation. 65 | 66 | 67 | -------------------------------------------------------------------------------- /CODE-OF-CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Community Code of Conduct 2 | 3 | Welcome to our open source project! 4 | 5 | We are committed to creating a friendly, respectful, and inclusive community. 6 | To ensure a positive experience in our project, we have established the following code of conduct, which we require all participants to abide by, and provide a safe and inclusive environment for all community members. 7 | 8 | ## Our Pledge 9 | 10 | As participants, contributors, and maintainers of our community, we pledge to: 11 | - Treat everyone with openness, inclusivity, and collaboration; 12 | - Respect individuals with different backgrounds and viewpoints, regardless of gender, sexual orientation, disability, race, ethnicity, religion, age, or any other factor; 13 | - Focus on contributing and improving the project, rather than attacking or criticizing individuals; 14 | - Build trust with community members and promote our project through constructive feedback; 15 | - Provide a safe, supportive, and encouraging environment for community members to promote learning and personal growth. 16 | 17 | ## Our standards 18 | 19 | Our community members should adhere to the following standards: 20 | - Respect the opinions, viewpoints, and experiences of others; 21 | - Avoid using insulting, discriminatory, or harmful language; 22 | - Do not harass, intimidate, or threaten others; 23 | - Do not publicly or privately disclose others' private information, such as contact information or addresses; 24 | - Respect the privacy of others; 25 | - Establish a safe, inclusive, and respectful environment for community members. 26 | 27 | ## Our responsibility 28 | 29 | Project maintainers have a responsibility to create a friendly, respectful, and inclusive environment for our community members. 30 | They should: 31 | - Clearly and publicly explain the community guidelines; 32 | - Handle reports of guideline violations and resolve disputes appropriately; 33 | - Protect the privacy and security of all community members; 34 | - Maintain a fair, transparent, and responsible attitude. 35 | 36 | 37 | ## Scope 38 | 39 | This code of conduct applies to all project spaces, including GitHub, mailing lists, forums, social media, gatherings, and conferences. 40 | Violations of the code of conduct will be dealt with, including but not limited to warnings, temporary or permanent bans, revocation of contribution rights, and revocation of project access rights. 41 | 42 | ## Implementation guidelines 43 | 44 | If you encounter behavior that violates this code of conduct, you can: 45 | - Communicate privately with the relevant person to try to resolve the issue; 46 | - Report the violation to the project maintainers(maintainer mailing list), who will take necessary action based on the situation; 47 | - If you are not satisfied with the way the maintainers handle the situation, you can seek help from higher-level organizations or institutions. 48 | 49 | Our community is a diverse, open, and inclusive community, and we welcome everyone's participation and contribution. 50 | We believe that only in a safe, respectful, and inclusive environment can we create the best project together. -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to Contribute 2 | 3 | Thank you for your interest and support in tRPC-A2A-go! 4 | 5 | We welcome and appreciate any form of contribution, including but not limited to submitting issues, providing improvement suggestions, improving documentation, fixing bugs, and adding features. 6 | This document aims to provide you with a detailed contribution guide to help you better participate in the project. 7 | Please read this guide carefully before contributing and make sure to follow the rules here. 8 | We look forward to working with you to make this project better together! 9 | 10 | ## Before contributing code 11 | 12 | The project welcomes code patches, but to make sure things are well coordinated you should discuss any significant change before starting the work. 13 | It's recommended that you signal your intention to contribute in the issue tracker, either by claiming an [existing one](https://github.com/trpc-group/trpc-a2a-go/issues) or by [opening a new issue](https://github.com/trpc-group/trpc-a2a-go/issues/new). 14 | 15 | ### Checking the issue tracker 16 | 17 | Whether you already know what contribution to make, or you are searching for an idea, the [issue tracker](https://github.com/trpc-group/trpc-a2a-go/issues) is always the first place to go. 18 | Issues are triaged to categorize them and manage the workflow. 19 | 20 | Most issues will be marked with one of the following workflow labels: 21 | - **NeedsInvestigation**: The issue is not fully understood and requires analysis to understand the root cause. 22 | - **NeedsDecision**: The issue is relatively well understood, but the tRPC-A2A-go team hasn't yet decided the best way to address it. 23 | It would be better to wait for a decision before writing code. 24 | If you are interested in working on an issue in this state, feel free to "ping" maintainers in the issue's comments if some time has passed without a decision. 25 | - **NeedsFix**: The issue is fully understood and code can be written to fix it. 26 | 27 | ### Opening an issue for any new problem 28 | 29 | Excluding very trivial changes, all contributions should be connected to an existing issue. 30 | Feel free to open one and discuss your plans. 31 | This process gives everyone a chance to validate the design, helps prevent duplication of effort, and ensures that the idea fits inside the goals for the language and tools. 32 | It also checks that the design is sound before code is written; the code review tool is not the place for high-level discussions. 33 | 34 | When opening an issue, make sure to answer these five questions: 35 | 1. What version of tRPC-A2A-go are you using ? 36 | 2. What operating system and processor architecture are you using(`go env`)? 37 | 3. What did you do? 38 | 4. What did you expect to see? 39 | 5. What did you see instead? 40 | 41 | For change proposals, see Proposing Changes To [tRPC-Proposals](https://github.com/trpc-group/trpc/tree/main/proposal). 42 | 43 | ## Contributing code 44 | 45 | Follow the [GitHub flow](https://docs.github.com/en/get-started/quickstart/github-flow) to [create a GitHub pull request](https://docs.github.com/en/get-started/quickstart/github-flow#create-a-pull-request). 46 | If this is your first time submitting a PR to the tRPC-A2A-go project, you will be reminded in the "Conversation" tab of the PR to sign and submit the [Contributor License Agreement](https://github.com/trpc-group/cla-database/blob/main/Tencent-Contributor-License-Agreement.md). 47 | Only when you have signed the Contributor License Agreement, your submitted PR has the possibility of being accepted. 48 | 49 | Some things to keep in mind: 50 | - Ensure that your code conforms to the project's code specifications. 51 | This includes but is not limited to code style, comment specifications, etc. This helps us to maintain the cleanliness and consistency of the project. 52 | - Before submitting a PR, please make sure that you have tested your code locally(`go test ./...`). 53 | Ensure that the code has no obvious errors and can run normally. 54 | - To update the pull request with new code, just push it to the branch; 55 | you can either add more commits, or rebase and force-push (both styles are accepted). 56 | - If the request is accepted, all commits will be squashed, and the final commit description will be composed by concatenating the pull request's title and description. 57 | The individual commits' descriptions will be discarded. 58 | See following "Write good commit messages" for some suggestions. 59 | 60 | ### Writing good commit messages 61 | 62 | Commit messages in tRPC-A2A-go follow a specific set of conventions, which we discuss in this section. 63 | 64 | Here is an example of a good one: 65 | 66 | 67 | > math: improve Sin, Cos and Tan precision for very large arguments 68 | > 69 | > The existing implementation has poor numerical properties for 70 | > large arguments, so use the McGillicutty algorithm to improve 71 | > accuracy above 1e10. 72 | > 73 | > The algorithm is described at https://wikipedia.org/wiki/McGillicutty_Algorithm 74 | > 75 | > Fixes #159 76 | > 77 | > RELEASE NOTES: Improved precision of Sin, Cos, and Tan for very large arguments (>1e10) 78 | 79 | #### First line 80 | 81 | The first line of the change description is conventionally a short one-line summary of the change, prefixed by the primary affected package. 82 | 83 | A rule of thumb is that it should be written so to complete the sentence "This change modifies tRPC-A2A-go to _____." 84 | That means it does not start with a capital letter, is not a complete sentence, and actually summarizes the result of the change. 85 | 86 | Follow the first line by a blank line. 87 | 88 | #### Main content 89 | 90 | The rest of the description elaborates and should provide context for the change and explain what it does. 91 | Write in complete sentences with correct punctuation, just like for your comments in tRPC-A2A-go. 92 | Don't use HTML, Markdown, or any other markup language. 93 | Add any relevant information, such as benchmark data if the change affects performance. 94 | The [benchstat](https://godoc.org/golang.org/x/perf/cmd/benchstat) tool is conventionally used to format benchmark data for change descriptions. 95 | 96 | #### Referencing issues 97 | 98 | The special notation "Fixes #12345" associates the change with issue 12345 in the tRPC-A2A-go issue tracker. 99 | When this change is eventually applied, the issue tracker will automatically mark the issue as fixed. 100 | 101 | - If there is a corresponding issue, add either `Fixes #12345` or `Updates #12345` (the latter if this is not a complete fix) to this comment 102 | - If referring to a repo other than `trpc-a2a-go` you can use the `owner/repo#issue_number` syntax: `Fixes trpc-group/tnet#12345` 103 | 104 | #### PR type label 105 | 106 | The PR type label is used to help identify the types of changes going into the release over time. This may allow the Release Team to develop a better understanding of what sorts of issues we would miss with a faster release cadence. 107 | 108 | For all pull requests, one of the following PR type labels must be set: 109 | 110 | - type/bug: Fixes a newly discovered bug. 111 | - type/enhancement: Adding tests, refactoring. 112 | - type/feature: New functionality. 113 | - type/documentation: Adds documentation. 114 | - type/api-change: Adds, removes, or changes an API. 115 | - type/failing-test: CI test case is showing intermittent failures. 116 | - type/performance: Changes that improves performance. 117 | - type/ci: Changes the CI configuration files and scripts. 118 | 119 | #### Release notes 120 | 121 | Release notes are required for any pull request with user-visible changes, this could mean: 122 | 123 | - User facing, critical bug-fixes 124 | - Notable feature additions 125 | - Deprecations or removals 126 | - API changes 127 | - Documents additions 128 | 129 | If the current PR doesn't have user-visible changes, such as internal code refactoring or adding test cases, the release notes should be filled with 'NONE' and the changes in this PR will not be recorded in the next version's CHANGELOG. If the current PR has user-visible changes, the release notes should be filled out according to the actual situation, avoiding technical details and describing the impact of the current changes from a user's perspective as much as possible. 130 | 131 | Release notes are one of the most important reference points for users about to import or upgrade to a particular release of tRPC-A2A-go. 132 | 133 | ## Miscellaneous topics 134 | 135 | ### Copyright headers 136 | 137 | Files in the tRPC-A2A-go repository don't list author names, both to avoid clutter and to avoid having to keep the lists up to date. 138 | Instead, your name will appear in the change log. 139 | 140 | New files that you contribute should use the standard copyright header: 141 | 142 | ```go 143 | // 144 | // 145 | // Tencent is pleased to support the open source community by making trpc-a2a-go available. 146 | // 147 | // Copyright (C) 2025 THL A29 Limited, a Tencent company. All rights reserved. 148 | // 149 | // trpc-a2a-go is licensed under the Apache License Version 2.0. 150 | // 151 | // 152 | ``` 153 | 154 | Files in the repository are copyrighted the year they are added. 155 | Do not update the copyright year on files that you change. -------------------------------------------------------------------------------- /CONTRIBUTORS.md: -------------------------------------------------------------------------------- 1 | # Contributor Guidelines 2 | 3 | Thank you for your interest and support for tRPC-A2A-go! 4 | This document outlines the roles and responsibilities of contributors in the project, as well as the process for becoming a Contributor and losing Maintainer status. We hope that this document will help every contributor understand the growth path and make a greater contribution to the project's development. 5 | 6 | ## Contributor Roles and Responsibilities 7 | 8 | we have two main contributor roles: Contributor and Maintainer. 9 | Here is a brief introduction to these two roles: 10 | 1. Contributor: A contributor to the project who can contribute code, documentation, testing, and other resources. Contributors provide valuable resources to the project, helping it to continuously improve and develop. 11 | 2. Maintainer: A maintainer of the project who is responsible for the day-to-day maintenance of the project, including reviewing and merging PRs, handling issues, and releasing versions. Maintainers are key members of the project and have a significant impact on the project's development direction and decision-making. 12 | 13 | ## How to become a Maintainer 14 | 15 | We welcome every contributor to contribute to the project's development and encourage contributors to upgrade to the role of Maintainer. 16 | The following are the conditions for upgrading from Contributor to Maintainer: 17 | 1. Continuous contribution: Contributors need to contribute to the project continuously for a period of time (e.g., 3 months). This demonstrates the contributor's attention and enthusiasm for the project. 18 | 2. Quality assurance: The code or documentation submitted by contributors needs to maintain a high level of quality, meet the project's specifications, and have a positive impact on the project. 19 | 3. Active participation: Contributors need to actively participate in project discussions and decision-making, providing constructive opinions and suggestions for the project's development. 20 | 4. Team collaboration: Contributors need to have good teamwork skills, communicate friendly with other contributors and maintainers, and work together to solve problems. 21 | 5. Responsibility: Contributors need to have a certain sense of responsibility and be willing to undertake some of the project maintenance work, including reviewing PRs and handling issues. When a contributor meets the above conditions, existing maintainers will evaluate them. 22 | 23 | If they meet the requirements of Maintainer, they will be invited to become a new Maintainer. 24 | 25 | ## Losing Maintainers status 26 | 27 | Maintainer have important responsibilities in the project, and we hope that every Maintainer can maintain their attention and enthusiasm for the project. 28 | However, we also understand that everyone's time and energy are limited, so when Maintainers cannot continue to fulfill their responsibilities, they will be downgraded to the role of Contributor: 29 | 1. Long-term inactivity: If a Maintainer has not participated in project maintenance work, including reviewing PRs and handling issues, for a period of time (e.g., 3 months), they will be considered inactive. 30 | 2. Quality issues: If a Maintainer's work in the project has serious quality issues that affect the project's development, they will be considered not meeting the requirements of Maintainer. 31 | 3. Team collaboration issues: If a Maintainer has serious communication or teamwork issues with other contributors and maintainers, such as disrespecting others' opinions, frequent conflicts, or refusing to collaborate, which affects the project's normal operation and atmosphere, they will be considered not meeting the requirements of Maintainer. 32 | 4. Violation of rules: If a Maintainer violates the project's rules or code of conduct, including but not limited to leaking sensitive information or abusing privileges, they will be considered not meeting the requirements of Maintainer. 33 | 5. Voluntary application: If a Maintainer cannot continue to fulfill their responsibilities due to personal reasons, they can voluntarily apply to be downgraded to the role of Contributor. -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Tencent is pleased to support the open source community by making trpc-a2a-go available. 2 | 3 | Copyright (C) 2025 THL A29 Limited, a Tencent company. All rights reserved. 4 | 5 | trpc-a2a-go is licensed under the Apache License Version 2.0. 6 | 7 | 8 | Terms of the Apache License Version 2.0: 9 | -------------------------------------------------------------------- 10 | Apache License 11 | 12 | Version 2.0, January 2004 13 | 14 | http://www.apache.org/licenses/ 15 | 16 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 17 | 1. Definitions. 18 | 19 | "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. 20 | 21 | "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. 22 | 23 | "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. 24 | 25 | "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. 28 | 29 | "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. 30 | 31 | "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). 32 | 33 | "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. 34 | 35 | "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." 36 | 37 | "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 38 | 39 | 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 40 | 41 | 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 42 | 43 | 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: 44 | 45 | You must give any other recipients of the Work or Derivative Works a copy of this License; and 46 | 47 | You must cause any modified files to carry prominent notices stating that You changed the files; and 48 | 49 | You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and 50 | 51 | If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. 52 | 53 | You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 54 | 55 | 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 56 | 57 | 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 58 | 59 | 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 60 | 61 | 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 62 | 63 | 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. 64 | 65 | END OF TERMS AND CONDITIONS 66 | -------------------------------------------------------------------------------- /client/options.go: -------------------------------------------------------------------------------- 1 | // Tencent is pleased to support the open source community by making trpc-a2a-go available. 2 | // 3 | // Copyright (C) 2025 THL A29 Limited, a Tencent company. All rights reserved. 4 | // 5 | // trpc-a2a-go is licensed under the Apache License Version 2.0. 6 | 7 | package client 8 | 9 | import ( 10 | "context" 11 | "net/http" 12 | "time" 13 | 14 | "golang.org/x/oauth2" 15 | "trpc.group/trpc-go/trpc-a2a-go/auth" 16 | ) 17 | 18 | // Option is a functional option type for configuring the A2AClient. 19 | type Option func(*A2AClient) 20 | 21 | // HTTPReqHandler is a custom HTTP request handler for a2a client. 22 | type HTTPReqHandler interface { 23 | Handle(ctx context.Context, client *http.Client, req *http.Request) (*http.Response, error) 24 | } 25 | 26 | // WithHTTPClient sets a custom http.Client for the A2AClient. 27 | func WithHTTPClient(client *http.Client) Option { 28 | return func(c *A2AClient) { 29 | if client != nil { 30 | c.httpClient = client 31 | } 32 | } 33 | } 34 | 35 | // WithTimeout sets the timeout for the underlying http.Client. 36 | // If a custom client was provided via WithHTTPClient, this modifies its timeout. 37 | func WithTimeout(timeout time.Duration) Option { 38 | return func(c *A2AClient) { 39 | if timeout > 0 && c.httpClient != nil { 40 | c.httpClient.Timeout = timeout 41 | } 42 | } 43 | } 44 | 45 | // WithUserAgent sets a custom User-Agent header for requests. 46 | func WithUserAgent(userAgent string) Option { 47 | return func(c *A2AClient) { 48 | c.userAgent = userAgent 49 | } 50 | } 51 | 52 | // Authentication options 53 | 54 | // WithJWTAuth configures the client to use JWT authentication. 55 | func WithJWTAuth(secret []byte, audience, issuer string, lifetime time.Duration) Option { 56 | return func(c *A2AClient) { 57 | provider := auth.NewJWTAuthProvider(secret, audience, issuer, lifetime) 58 | c.authProvider = provider 59 | c.httpClient = provider.ConfigureClient(c.httpClient) 60 | } 61 | } 62 | 63 | // WithAPIKeyAuth configures the client to use API key authentication. 64 | func WithAPIKeyAuth(apiKey, headerName string) Option { 65 | return func(c *A2AClient) { 66 | provider := auth.NewAPIKeyAuthProvider(make(map[string]string), headerName) 67 | provider.SetClientAPIKey(apiKey) 68 | c.authProvider = provider 69 | c.httpClient = provider.ConfigureClient(c.httpClient) 70 | } 71 | } 72 | 73 | // WithOAuth2ClientCredentials configures the client to use OAuth2 client credentials flow. 74 | func WithOAuth2ClientCredentials(clientID, clientSecret, tokenURL string, scopes []string) Option { 75 | return func(c *A2AClient) { 76 | provider := auth.NewOAuth2ClientCredentialsProvider( 77 | clientID, 78 | clientSecret, 79 | tokenURL, 80 | scopes, 81 | ) 82 | c.authProvider = provider 83 | c.httpClient = provider.ConfigureClient(c.httpClient) 84 | } 85 | } 86 | 87 | // WithOAuth2TokenSource configures the client to use a custom OAuth2 token source. 88 | func WithOAuth2TokenSource(config *oauth2.Config, tokenSource oauth2.TokenSource) Option { 89 | return func(c *A2AClient) { 90 | provider := auth.NewOAuth2AuthProviderWithConfig(config, "", "") 91 | provider.SetTokenSource(tokenSource) 92 | c.authProvider = provider 93 | c.httpClient = provider.ConfigureClient(c.httpClient) 94 | } 95 | } 96 | 97 | // WithAuthProvider allows using a custom auth provider that implements the ClientProvider interface. 98 | func WithAuthProvider(provider auth.ClientProvider) Option { 99 | return func(c *A2AClient) { 100 | c.authProvider = provider 101 | c.httpClient = provider.ConfigureClient(c.httpClient) 102 | } 103 | } 104 | 105 | // WithHTTPReqHandler sets a custom HTTP request handler for the A2AClient. 106 | func WithHTTPReqHandler(handler HTTPReqHandler) Option { 107 | return func(c *A2AClient) { 108 | c.httpReqHandler = handler 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /client/options_test.go: -------------------------------------------------------------------------------- 1 | // Tencent is pleased to support the open source community by making trpc-a2a-go available. 2 | // 3 | // Copyright (C) 2025 THL A29 Limited, a Tencent company. All rights reserved. 4 | // 5 | // trpc-a2a-go is licensed under the Apache License Version 2.0. 6 | 7 | package client 8 | 9 | import ( 10 | "net/http" 11 | "testing" 12 | "time" 13 | 14 | "github.com/stretchr/testify/assert" 15 | "github.com/stretchr/testify/require" 16 | "golang.org/x/oauth2" 17 | "trpc.group/trpc-go/trpc-a2a-go/auth" 18 | ) 19 | 20 | func TestWithHTTPClient(t *testing.T) { 21 | // Create a custom HTTP client with specific timeout 22 | customClient := &http.Client{ 23 | Timeout: 30 * time.Second, 24 | } 25 | 26 | // Create A2A client with the custom HTTP client 27 | client, err := NewA2AClient("http://localhost:8080", WithHTTPClient(customClient)) 28 | require.NoError(t, err) 29 | 30 | // Verify the client is using our custom HTTP client 31 | assert.Equal(t, customClient, client.httpClient) 32 | assert.Equal(t, 30*time.Second, client.httpClient.Timeout) 33 | 34 | // Test with nil client (should use default) 35 | defaultClient, err := NewA2AClient("http://localhost:8080", WithHTTPClient(nil)) 36 | require.NoError(t, err) 37 | assert.NotNil(t, defaultClient.httpClient) 38 | assert.Equal(t, defaultTimeout, defaultClient.httpClient.Timeout) 39 | } 40 | 41 | func TestWithTimeout(t *testing.T) { 42 | // Create client with custom timeout 43 | client, err := NewA2AClient("http://localhost:8080", WithTimeout(45*time.Second)) 44 | require.NoError(t, err) 45 | 46 | // Verify timeout was set correctly 47 | assert.Equal(t, 45*time.Second, client.httpClient.Timeout) 48 | 49 | // Test with zero timeout (should use default) 50 | defaultClient, err := NewA2AClient("http://localhost:8080", WithTimeout(0)) 51 | require.NoError(t, err) 52 | assert.Equal(t, defaultTimeout, defaultClient.httpClient.Timeout) 53 | } 54 | 55 | func TestWithUserAgent(t *testing.T) { 56 | // Create client with custom user agent 57 | customUA := "CustomUserAgent/1.0" 58 | client, err := NewA2AClient("http://localhost:8080", WithUserAgent(customUA)) 59 | require.NoError(t, err) 60 | 61 | // Verify user agent was set correctly 62 | assert.Equal(t, customUA, client.userAgent) 63 | } 64 | 65 | func TestWithJWTAuth(t *testing.T) { 66 | secretKey := []byte("test-secret-key") 67 | audience := "test-audience" 68 | issuer := "test-issuer" 69 | lifetime := 1 * time.Hour 70 | 71 | // Create client with JWT auth 72 | client, err := NewA2AClient("http://localhost:8080", 73 | WithJWTAuth(secretKey, audience, issuer, lifetime)) 74 | require.NoError(t, err) 75 | 76 | // Verify auth provider was set up correctly 77 | assert.NotNil(t, client.authProvider) 78 | 79 | // Type assertion to check it's the right type 80 | jwtProvider, ok := client.authProvider.(*auth.JWTAuthProvider) 81 | assert.True(t, ok, "Should be a JWTAuthProvider") 82 | assert.Equal(t, secretKey, jwtProvider.Secret) 83 | assert.Equal(t, audience, jwtProvider.Audience) 84 | assert.Equal(t, issuer, jwtProvider.Issuer) 85 | assert.Equal(t, lifetime, jwtProvider.TokenLifetime) 86 | } 87 | 88 | func TestWithAPIKeyAuth(t *testing.T) { 89 | apiKey := "test-api-key" 90 | headerName := "X-Custom-API-Key" 91 | 92 | // Create client with API key auth 93 | client, err := NewA2AClient("http://localhost:8080", 94 | WithAPIKeyAuth(apiKey, headerName)) 95 | require.NoError(t, err) 96 | 97 | // Verify auth provider was set up correctly 98 | assert.NotNil(t, client.authProvider) 99 | 100 | // Type assertion to check it's the right type 101 | _, ok := client.authProvider.(*auth.APIKeyAuthProvider) 102 | assert.True(t, ok, "Should be an APIKeyAuthProvider") 103 | } 104 | 105 | func TestWithOAuth2ClientCredentials(t *testing.T) { 106 | clientID := "test-client-id" 107 | clientSecret := "test-client-secret-placeholder" 108 | tokenURL := "https://auth.example.com/token" 109 | scopes := []string{"profile", "email"} 110 | 111 | // Create client with OAuth2 client credentials 112 | client, err := NewA2AClient("http://localhost:8080", 113 | WithOAuth2ClientCredentials(clientID, clientSecret, tokenURL, scopes)) 114 | require.NoError(t, err) 115 | 116 | // Verify auth provider was set up correctly 117 | assert.NotNil(t, client.authProvider) 118 | 119 | // Type assertion to check it's the right type 120 | _, ok := client.authProvider.(*auth.OAuth2AuthProvider) 121 | assert.True(t, ok, "Should be an OAuth2AuthProvider") 122 | } 123 | 124 | func TestWithOAuth2TokenSource(t *testing.T) { 125 | // Create a test OAuth2 config 126 | config := &oauth2.Config{ 127 | ClientID: "test-client-id", 128 | ClientSecret: "test-client-secret-placeholder", 129 | Endpoint: oauth2.Endpoint{ 130 | TokenURL: "https://auth.example.com/token", 131 | }, 132 | } 133 | 134 | // Create a static token source 135 | tokenSource := oauth2.StaticTokenSource(&oauth2.Token{ 136 | AccessToken: "test-access-token-placeholder", 137 | TokenType: "Bearer", 138 | }) 139 | 140 | // Create client with OAuth2 token source 141 | client, err := NewA2AClient("http://localhost:8080", 142 | WithOAuth2TokenSource(config, tokenSource)) 143 | require.NoError(t, err) 144 | 145 | // Verify auth provider was set up correctly 146 | assert.NotNil(t, client.authProvider) 147 | 148 | // Type assertion to check it's the right type 149 | _, ok := client.authProvider.(*auth.OAuth2AuthProvider) 150 | assert.True(t, ok, "Should be an OAuth2AuthProvider") 151 | } 152 | 153 | func TestWithAuthProvider(t *testing.T) { 154 | // Create a mock auth provider 155 | mockProvider := &mockClientProvider{} 156 | 157 | // Create client with custom auth provider 158 | client, err := NewA2AClient("http://localhost:8080", 159 | WithAuthProvider(mockProvider)) 160 | require.NoError(t, err) 161 | 162 | // Verify auth provider was set correctly 163 | assert.Equal(t, mockProvider, client.authProvider) 164 | } 165 | 166 | // mockClientProvider implements auth.ClientProvider for testing 167 | type mockClientProvider struct{} 168 | 169 | func (p *mockClientProvider) Authenticate(r *http.Request) (*auth.User, error) { 170 | return &auth.User{ID: "mock-user"}, nil 171 | } 172 | 173 | func (p *mockClientProvider) ConfigureClient(client *http.Client) *http.Client { 174 | return client 175 | } 176 | -------------------------------------------------------------------------------- /examples/auth/client/main.go: -------------------------------------------------------------------------------- 1 | // Tencent is pleased to support the open source community by making trpc-a2a-go available. 2 | // 3 | // Copyright (C) 2025 THL A29 Limited, a Tencent company. All rights reserved. 4 | // 5 | // trpc-a2a-go is licensed under the Apache License Version 2.0. 6 | 7 | // Package main provides example code for using different authentication methods with the A2A client. 8 | package main 9 | 10 | import ( 11 | "context" 12 | "flag" 13 | "fmt" 14 | "log" 15 | "os" 16 | "strings" 17 | "time" 18 | 19 | "golang.org/x/oauth2/clientcredentials" 20 | "trpc.group/trpc-go/trpc-a2a-go/auth" 21 | "trpc.group/trpc-go/trpc-a2a-go/client" 22 | "trpc.group/trpc-go/trpc-a2a-go/protocol" 23 | ) 24 | 25 | // config holds the client configuration options. 26 | type config struct { 27 | AuthMethod string 28 | AgentURL string 29 | Timeout time.Duration 30 | 31 | // JWT Auth options 32 | JWTSecret string 33 | JWTSecretFile string 34 | JWTAudience string 35 | JWTIssuer string 36 | JWTExpiry time.Duration 37 | 38 | // API Key options 39 | APIKey string 40 | APIKeyHeader string 41 | 42 | // OAuth2 options 43 | OAuth2ClientID string 44 | OAuth2ClientSecret string 45 | OAuth2TokenURL string 46 | OAuth2Scopes string 47 | 48 | // Task options 49 | TaskID string 50 | TaskMessage string 51 | SessionID string 52 | } 53 | 54 | func main() { 55 | config := parseFlags() 56 | 57 | if config.AuthMethod == "" { 58 | flag.Usage() 59 | return 60 | } 61 | 62 | var a2aClient *client.A2AClient 63 | var err error 64 | 65 | // Create client with the specified authentication method 66 | switch config.AuthMethod { 67 | case "jwt": 68 | a2aClient, err = createJWTClient(config) 69 | case "apikey": 70 | a2aClient, err = createAPIKeyClient(config) 71 | case "oauth2": 72 | a2aClient, err = createOAuth2Client(config) 73 | default: 74 | fmt.Printf("Unknown authentication method: %s\n", config.AuthMethod) 75 | return 76 | } 77 | 78 | if err != nil { 79 | log.Fatalf("Failed to create client: %v", err) 80 | } 81 | 82 | // Create a simple task to test authentication 83 | textPart := protocol.NewTextPart(config.TaskMessage) 84 | message := protocol.NewMessage(protocol.MessageRoleUser, []protocol.Part{textPart}) 85 | 86 | // Prepare task parameters 87 | taskParams := protocol.SendTaskParams{ 88 | ID: config.TaskID, 89 | Message: message, 90 | } 91 | 92 | // Add session ID if provided 93 | if config.SessionID != "" { 94 | taskParams.SessionID = &config.SessionID 95 | } 96 | 97 | // Send the task 98 | ctx, cancel := context.WithTimeout(context.Background(), config.Timeout) 99 | defer cancel() 100 | 101 | task, err := a2aClient.SendTasks(ctx, taskParams) 102 | 103 | if err != nil { 104 | log.Fatalf("Failed to send task: %v", err) 105 | } 106 | 107 | fmt.Printf("Task ID: %s, Status: %s\n", task.ID, task.Status.State) 108 | if task.SessionID != nil { 109 | fmt.Printf("Session ID: %s\n", *task.SessionID) 110 | } 111 | 112 | // For demonstration purposes, get the task status 113 | taskQuery := protocol.TaskQueryParams{ 114 | ID: task.ID, 115 | } 116 | 117 | updatedTask, err := a2aClient.GetTasks(ctx, taskQuery) 118 | if err != nil { 119 | log.Fatalf("Failed to get task: %v", err) 120 | } 121 | 122 | fmt.Printf("Updated task status: %s\n", updatedTask.Status.State) 123 | } 124 | 125 | // parseFlags parses command-line flags and returns a Config. 126 | func parseFlags() config { 127 | var config config 128 | 129 | // Basic options 130 | flag.StringVar(&config.AuthMethod, "auth", "jwt", "Authentication method (jwt, apikey, oauth2)") 131 | flag.StringVar(&config.AgentURL, "url", "http://localhost:8080/", "Target A2A agent URL") 132 | flag.DurationVar(&config.Timeout, "timeout", 60*time.Second, "Request timeout") 133 | 134 | // JWT options 135 | flag.StringVar(&config.JWTSecret, "jwt-secret", "my-secret-key", "JWT secret key") 136 | flag.StringVar(&config.JWTSecretFile, "jwt-secret-file", "../server/jwt-secret.key", "File containing JWT secret key") 137 | flag.StringVar(&config.JWTAudience, "jwt-audience", "a2a-server", "JWT audience") 138 | flag.StringVar(&config.JWTIssuer, "jwt-issuer", "example", "JWT issuer") 139 | flag.DurationVar(&config.JWTExpiry, "jwt-expiry", 1*time.Hour, "JWT expiration time") 140 | 141 | // API Key options 142 | flag.StringVar(&config.APIKey, "api-key", "test-api-key", "API key") 143 | flag.StringVar(&config.APIKeyHeader, "api-key-header", "X-API-Key", "API key header name") 144 | 145 | // OAuth2 options 146 | flag.StringVar(&config.OAuth2ClientID, "oauth2-client-id", "my-client-id", "OAuth2 client ID") 147 | flag.StringVar(&config.OAuth2ClientSecret, "oauth2-client-secret", "my-client-secret", "OAuth2 client secret") 148 | flag.StringVar(&config.OAuth2TokenURL, "oauth2-token-url", "", "OAuth2 token URL (default: derived from agent URL)") 149 | flag.StringVar(&config.OAuth2Scopes, "oauth2-scopes", "a2a.read,a2a.write", "OAuth2 scopes (comma-separated)") 150 | 151 | // Task options 152 | flag.StringVar(&config.TaskID, "task-id", "auth-test-task", "ID for the task to send") 153 | flag.StringVar(&config.TaskMessage, "message", "Hello, this is an authenticated request", "Message to send") 154 | flag.StringVar(&config.SessionID, "session-id", "", "Optional session ID for the task") 155 | 156 | flag.Parse() 157 | 158 | return config 159 | } 160 | 161 | // getJWTSecret retrieves the JWT secret from either the direct key or a file. 162 | func getJWTSecret(config config) ([]byte, error) { 163 | // If a secret file is provided, read from it 164 | if config.JWTSecretFile != "" { 165 | secret, err := os.ReadFile(config.JWTSecretFile) 166 | if err != nil { 167 | return nil, fmt.Errorf("failed to read JWT secret file: %w", err) 168 | } 169 | return secret, nil 170 | } 171 | 172 | // Otherwise use the direct secret value 173 | return []byte(config.JWTSecret), nil 174 | } 175 | 176 | // createJWTClient creates an A2A client with JWT authentication. 177 | func createJWTClient(config config) (*client.A2AClient, error) { 178 | secret, err := getJWTSecret(config) 179 | if err != nil { 180 | return nil, err 181 | } 182 | 183 | return client.NewA2AClient( 184 | config.AgentURL, 185 | client.WithJWTAuth(secret, config.JWTAudience, config.JWTIssuer, config.JWTExpiry), 186 | ) 187 | } 188 | 189 | // createAPIKeyClient creates an A2A client with API key authentication. 190 | func createAPIKeyClient(config config) (*client.A2AClient, error) { 191 | return client.NewA2AClient( 192 | config.AgentURL, 193 | client.WithAPIKeyAuth(config.APIKey, config.APIKeyHeader), 194 | ) 195 | } 196 | 197 | // createOAuth2Client creates an A2A client with OAuth2 authentication. 198 | func createOAuth2Client(config config) (*client.A2AClient, error) { 199 | // Method 1: Using client credentials flow 200 | return createOAuth2ClientCredentialsClient(config) 201 | 202 | // Alternative methods: 203 | // return createOAuth2TokenSourceClient(config) 204 | // return createCustomOAuth2Client(config) 205 | } 206 | 207 | // createOAuth2ClientCredentialsClient creates a client using OAuth2 client credentials flow. 208 | func createOAuth2ClientCredentialsClient(config config) (*client.A2AClient, error) { 209 | // Determine token URL if not specified 210 | tokenURL := config.OAuth2TokenURL 211 | if tokenURL == "" { 212 | tokenURL = getOAuthTokenURL(config.AgentURL) 213 | } 214 | 215 | // Parse scopes 216 | scopes := []string{} 217 | if config.OAuth2Scopes != "" { 218 | for _, scope := range strings.Split(config.OAuth2Scopes, ",") { 219 | scopes = append(scopes, strings.TrimSpace(scope)) 220 | } 221 | } 222 | 223 | return client.NewA2AClient( 224 | config.AgentURL, 225 | client.WithOAuth2ClientCredentials(config.OAuth2ClientID, config.OAuth2ClientSecret, tokenURL, scopes), 226 | ) 227 | } 228 | 229 | // createOAuth2TokenSourceClient creates a client using a custom OAuth2 token source. 230 | func createOAuth2TokenSourceClient(config config) (*client.A2AClient, error) { 231 | // Extract the OAuth token URL from agentURL 232 | tokenURL := getOAuthTokenURL(config.AgentURL) 233 | 234 | // Example with password credentials grant 235 | config.OAuth2TokenURL = tokenURL 236 | config.OAuth2Scopes = "a2a.read,a2a.write" 237 | 238 | return createOAuth2ClientCredentialsClient(config) 239 | } 240 | 241 | // createCustomOAuth2Client creates a client with a completely custom OAuth2 provider. 242 | func createCustomOAuth2Client(config config) (*client.A2AClient, error) { 243 | // Extract the OAuth token URL from agentURL 244 | tokenURL := getOAuthTokenURL(config.AgentURL) 245 | 246 | // Create a client credentials config 247 | ccConfig := &clientcredentials.Config{ 248 | ClientID: config.OAuth2ClientID, 249 | ClientSecret: config.OAuth2ClientSecret, 250 | TokenURL: tokenURL, 251 | Scopes: []string{config.OAuth2Scopes}, 252 | } 253 | 254 | // Create a custom OAuth2 provider 255 | provider := auth.NewOAuth2ClientCredentialsProvider( 256 | ccConfig.ClientID, 257 | ccConfig.ClientSecret, 258 | ccConfig.TokenURL, 259 | ccConfig.Scopes, 260 | ) 261 | 262 | // Use the custom provider 263 | return client.NewA2AClient( 264 | config.AgentURL, 265 | client.WithAuthProvider(provider), 266 | ) 267 | } 268 | 269 | // getOAuthTokenURL is a helper function to get the OAuth token URL based on agent URL. 270 | func getOAuthTokenURL(agentURL string) string { 271 | tokenURL := "" 272 | if agentURL == "http://localhost:8080/" { 273 | tokenURL = "http://localhost:8080/oauth2/token" 274 | } else { 275 | // Try to adapt to a different port 276 | // This is a simple adaptation, not fully robust 277 | tokenURL = agentURL + "oauth2/token" 278 | if tokenURL[len(tokenURL)-1] == '/' { 279 | tokenURL = tokenURL[:len(tokenURL)-1] 280 | } 281 | } 282 | fmt.Printf("Using OAuth2 token URL: %s\n", tokenURL) 283 | return tokenURL 284 | } 285 | -------------------------------------------------------------------------------- /examples/basic/README.md: -------------------------------------------------------------------------------- 1 | # A2A Basic Example 2 | 3 | This example demonstrates a basic implementation of the Agent-to-Agent (A2A) protocol using the trpc-a2a-go library. It consists of: 4 | 5 | 1. A versatile text processing agent server that supports multiple operations 6 | 2. A feature-rich CLI client that demonstrates all the core A2A protocol APIs 7 | 8 | ## Server Features 9 | 10 | The server is a text processing agent capable of: 11 | 12 | - Processing text in various modes: reverse, uppercase, lowercase, word count 13 | - Supporting both streaming and non-streaming responses 14 | - Demonstrating multi-turn conversations with the `input-required` state 15 | - Handling task cancellation 16 | - Creating and streaming artifacts 17 | 18 | ### Running the Server 19 | 20 | ```bash 21 | cd server 22 | go run main.go [options] 23 | ``` 24 | 25 | Server options: 26 | - `--host`: Host address (default: localhost) 27 | - `--port`: Port number (default: 8080) 28 | - `--desc`: Custom agent description 29 | - `--no-cors`: Disable CORS headers 30 | - `--no-stream`: Disable streaming capability 31 | 32 | ## Client Features 33 | 34 | The client is a CLI application that connects to an A2A agent and provides: 35 | 36 | - Support for both streaming and non-streaming modes 37 | - Interactive CLI with command history 38 | - Session management for contextual conversations 39 | - Task management (create, cancel, get) 40 | - Agent capability discovery 41 | 42 | ### Running the Client 43 | 44 | ```bash 45 | cd client 46 | go run main.go [options] 47 | ``` 48 | 49 | Client options: 50 | - `--agent`: Agent URL (default: http://localhost:8080/) 51 | - `--timeout`: Request timeout (default: 60s) 52 | - `--no-stream`: Disable streaming mode 53 | - `--session`: Use specific session ID (generate new if empty) 54 | - `--use-tasks-get`: Use tasks/get to fetch final state (default: true) 55 | - `--history`: Number of history messages to request (default: 0) 56 | 57 | ### Client Commands 58 | 59 | Once the client is running, you can use the following commands: 60 | 61 | - `help`: Show help message 62 | - `exit`: Exit the program 63 | - `session [id]`: Set or generate a new session ID 64 | - `mode [stream|sync]`: Set interaction mode (streaming or standard) 65 | - `cancel [task-id]`: Cancel a task 66 | - `get [task-id] [history]`: Get task details 67 | - `card`: Fetch and display the agent's capabilities card 68 | 69 | For normal interaction, simply type your message and press Enter. 70 | 71 | ### Text Processing Commands 72 | 73 | The server understands the following text processing commands: 74 | 75 | - `reverse `: Reverses the input text 76 | - `uppercase `: Converts text to uppercase 77 | - `lowercase `: Converts text to lowercase 78 | - `count `: Counts words and characters in text 79 | - `multi`: Start a multi-step interaction 80 | - `example`: Demonstrates input-required state 81 | - `help`: Shows the help message 82 | 83 | ## Usage Example 84 | 85 | 1. Start the server: 86 | ```bash 87 | cd server 88 | go run main.go 89 | ``` 90 | 91 | 2. In another terminal, start the client: 92 | ```bash 93 | cd client 94 | go run main.go 95 | ``` 96 | 97 | 3. Try some commands: 98 | ``` 99 | > help 100 | > reverse hello world 101 | > uppercase the quick brown fox 102 | > multi 103 | > card 104 | > mode sync 105 | > lowercase TESTING LOWERCASE 106 | ``` 107 | 108 | ## A2A Protocol Implementation 109 | 110 | This example demonstrates the following A2A protocol features: 111 | 112 | - Agent discovery via Agent Cards (/.well-known/agent.json) 113 | - Task creation using tasks/send and tasks/sendSubscribe 114 | - Task state retrieval using tasks/get 115 | - Task cancellation using tasks/cancel 116 | - Streaming updates for long-running tasks 117 | - Multi-turn conversations using the input-required state 118 | - Artifact generation and streaming 119 | 120 | The implementation follows the A2A specification and provides a practical example of building interoperable AI agents using the protocol. -------------------------------------------------------------------------------- /examples/go.mod: -------------------------------------------------------------------------------- 1 | module trpc.group/trpc-go/trpc-a2a-go/examples 2 | 3 | go 1.23.0 4 | 5 | toolchain go1.23.7 6 | 7 | require ( 8 | github.com/google/uuid v1.3.1 9 | github.com/lestrrat-go/jwx/v2 v2.1.4 10 | golang.org/x/oauth2 v0.29.0 11 | trpc.group/trpc-go/trpc-a2a-go v0.0.0 12 | ) 13 | 14 | require ( 15 | github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect 16 | github.com/goccy/go-json v0.10.3 // indirect 17 | github.com/golang-jwt/jwt/v5 v5.2.2 // indirect 18 | github.com/lestrrat-go/blackmagic v1.0.2 // indirect 19 | github.com/lestrrat-go/httpcc v1.0.1 // indirect 20 | github.com/lestrrat-go/httprc v1.0.6 // indirect 21 | github.com/lestrrat-go/iter v1.0.2 // indirect 22 | github.com/lestrrat-go/option v1.0.1 // indirect 23 | github.com/segmentio/asm v1.2.0 // indirect 24 | go.uber.org/multierr v1.10.0 // indirect 25 | go.uber.org/zap v1.27.0 // indirect 26 | golang.org/x/crypto v0.35.0 // indirect 27 | golang.org/x/sys v0.30.0 // indirect 28 | ) 29 | 30 | replace trpc.group/trpc-go/trpc-a2a-go => ../ 31 | -------------------------------------------------------------------------------- /examples/go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc= 5 | github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40= 6 | github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA= 7 | github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= 8 | github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= 9 | github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= 10 | github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= 11 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 12 | github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4= 13 | github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 14 | github.com/lestrrat-go/blackmagic v1.0.2 h1:Cg2gVSc9h7sz9NOByczrbUvLopQmXrfFx//N+AkAr5k= 15 | github.com/lestrrat-go/blackmagic v1.0.2/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU= 16 | github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE= 17 | github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E= 18 | github.com/lestrrat-go/httprc v1.0.6 h1:qgmgIRhpvBqexMJjA/PmwSvhNk679oqD1RbovdCGW8k= 19 | github.com/lestrrat-go/httprc v1.0.6/go.mod h1:mwwz3JMTPBjHUkkDv/IGJ39aALInZLrhBp0X7KGUZlo= 20 | github.com/lestrrat-go/iter v1.0.2 h1:gMXo1q4c2pHmC3dn8LzRhJfP1ceCbgSiT9lUydIzltI= 21 | github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4= 22 | github.com/lestrrat-go/jwx/v2 v2.1.4 h1:uBCMmJX8oRZStmKuMMOFb0Yh9xmEMgNJLgjuKKt4/qc= 23 | github.com/lestrrat-go/jwx/v2 v2.1.4/go.mod h1:nWRbDFR1ALG2Z6GJbBXzfQaYyvn751KuuyySN2yR6is= 24 | github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU= 25 | github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= 26 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 27 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 28 | github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys= 29 | github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= 30 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 31 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 32 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 33 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 34 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 35 | go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= 36 | go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= 37 | go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= 38 | go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= 39 | go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= 40 | go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= 41 | golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs= 42 | golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ= 43 | golang.org/x/oauth2 v0.29.0 h1:WdYw2tdTK1S8olAzWHdgeqfy+Mtm9XNhv/xJsY65d98= 44 | golang.org/x/oauth2 v0.29.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= 45 | golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= 46 | golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 47 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 48 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 49 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 50 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 51 | -------------------------------------------------------------------------------- /examples/jwks/README.md: -------------------------------------------------------------------------------- 1 | # JWT-Based Push Notifications with JWKS Example 2 | 3 | This example demonstrates how to implement secure push notifications using JWT (JSON Web Tokens) with JWKS (JSON Web Key Set) in an A2A application, specifically for handling asynchronous task processing. 4 | 5 | ## Overview 6 | 7 | The example showcases a robust approach for long-running tasks with secure notifications: 8 | 9 | 1. Client sends a task via the non-streaming API (`tasks/send`) 10 | 2. Client registers a webhook URL to receive push notifications 11 | 3. Server processes the task asynchronously in the background 12 | 4. When the task completes, the server sends a cryptographically signed push notification 13 | 5. Client verifies the notification's authenticity using JWKS before processing it 14 | 15 | This pattern is ideal for: 16 | - Long-running tasks that would exceed typical HTTP request timeouts 17 | - Situations where maintaining persistent connections is impractical 18 | - Asynchronous workflows requiring secure completion notifications 19 | - Scenarios where notification authenticity must be cryptographically verified 20 | 21 | ## Components 22 | 23 | The example consists of two primary components: 24 | 25 | ### Server Component 26 | - Generates RSA key pairs for JWT signing 27 | - Exposes a JWKS endpoint (`.well-known/jwks.json`) to share public keys 28 | - Processes tasks asynchronously in separate goroutines 29 | - Signs notifications with JWT (includes task ID, timestamp, and payload hash) 30 | - Sends authenticated push notifications for task status changes 31 | 32 | ### Client Component 33 | - Hosts a webhook server to receive push notifications 34 | - Fetches and caches public keys from the server's JWKS endpoint 35 | - Verifies JWT signatures on incoming notifications 36 | - Includes fallback verification for improved compatibility 37 | - Validates payload hash to prevent tampering 38 | - Tracks and displays task status changes 39 | 40 | ## Security Features 41 | 42 | - **RSA-Based Cryptographic Signatures**: Uses RS256 algorithm for secure signing 43 | - **JWKS for Key Distribution**: Standardized method to share public keys 44 | - **Key ID Support**: Allows for seamless key rotation 45 | - **Payload Hash Verification**: Prevents notification content tampering 46 | - **Token Expiration Checking**: Prevents replay attacks 47 | - **Automatic Key Refresh**: Periodically updates JWKS from the server 48 | 49 | ## Running the Example 50 | 51 | ### Start the Server 52 | 53 | ```bash 54 | go run server/main.go 55 | ``` 56 | 57 | Configure with optional flags: 58 | ```bash 59 | go run server/main.go -port 8000 -notify-host localhost 60 | ``` 61 | 62 | ### Start the Client 63 | 64 | ```bash 65 | go run client/main.go 66 | ``` 67 | 68 | Configure with optional flags: 69 | ```bash 70 | go run client/main.go -server-host localhost -server-port 8000 -webhook-host localhost -webhook-port 8001 -webhook-path /webhook 71 | ``` 72 | 73 | ## Authentication Flow 74 | 75 | 1. **Key Generation**: Server generates RSA key pairs and assigns key IDs 76 | 2. **JWKS Publication**: Server exposes public keys via JWKS endpoint 77 | 3. **Task Registration**: Client registers webhook URL and sends a task 78 | 4. **Task Processing**: Server processes task asynchronously 79 | 5. **Notification Signing**: Server signs notification with private key and includes: 80 | - Task ID and status 81 | - Timestamp 82 | - Payload hash (SHA-256) 83 | - Key ID in JWT header 84 | 6. **JWT Verification**: Client verifies signature using public key from JWKS 85 | 7. **Payload Verification**: Client verifies payload hash matches content 86 | 87 | ## Advanced Features 88 | 89 | - **Flexible Verification**: Multiple verification methods for compatibility 90 | - **Debug Logging**: Comprehensive logging for debugging JWT issues 91 | - **Periodic Key Refresh**: Background refresh of JWKS to handle key rotation 92 | - **Task Status Tracking**: Client-side tracking of task state transitions 93 | - **Enhanced Error Handling**: Detailed error reporting for authentication issues 94 | 95 | ## API Usage 96 | 97 | The example demonstrates these A2A API features: 98 | 99 | - `server.NewA2AServer()` - Create an A2A server 100 | - `server.WithJWKSEndpoint()` - Enable JWKS endpoint 101 | - `server.WithPushNotificationAuthenticator()` - Configure JWT authentication 102 | - `a2aClient.SendTasks()` - Send task via non-streaming API 103 | - `a2aClient.SetPushNotification()` - Register webhook for notifications 104 | 105 | ## License 106 | 107 | This example is released under the Apache License Version 2.0, the same license as the trpc-a2a-go project. -------------------------------------------------------------------------------- /examples/jwks/server/main.go: -------------------------------------------------------------------------------- 1 | // Tencent is pleased to support the open source community by making trpc-a2a-go available. 2 | // 3 | // Copyright (C) 2025 THL A29 Limited, a Tencent company. All rights reserved. 4 | // 5 | // trpc-a2a-go is licensed under the Apache License Version 2.0. 6 | 7 | // Package main implements a server with push notification support using JWKS. 8 | // The server demonstrates how to set up and use push notifications in an A2A server: 9 | // 10 | // 1. Push notifications are enabled in the server factory via NewPushNotificationServer 11 | // 2. The task processor implements the PushNotificationProcessor interface 12 | // 3. When a task reaches a terminal state, the task manager sends a push notification 13 | // 4. Notifications are signed using JWT with the private key in the authenticator 14 | // 5. Clients can verify the notification using the public key from the JWKS endpoint 15 | package main 16 | 17 | import ( 18 | "context" 19 | "encoding/json" 20 | "flag" 21 | "fmt" 22 | "time" 23 | 24 | "trpc.group/trpc-go/trpc-a2a-go/auth" 25 | "trpc.group/trpc-go/trpc-a2a-go/log" 26 | "trpc.group/trpc-go/trpc-a2a-go/protocol" 27 | "trpc.group/trpc-go/trpc-a2a-go/server" 28 | "trpc.group/trpc-go/trpc-a2a-go/taskmanager" 29 | ) 30 | 31 | const ( 32 | defaultServerPort = 8000 33 | defaultNotifyHost = "localhost" 34 | ) 35 | 36 | // pushNotificationTaskProcessor is a task processor that sends push notifications. 37 | // It implements both the taskmanager.TaskProcessor interface for task processing 38 | // and the PushNotificationProcessor interface for receiving the authenticator. 39 | type pushNotificationTaskProcessor struct { 40 | notifyHost string 41 | manager *pushNotificationTaskManager 42 | } 43 | 44 | // Process implements the TaskProcessor interface. 45 | // This method should return quickly, only setting the task to "submitted" state 46 | // and then processing the task asynchronously. 47 | func (p *pushNotificationTaskProcessor) Process( 48 | ctx context.Context, 49 | taskID string, 50 | message protocol.Message, 51 | handle taskmanager.TaskHandle, 52 | ) error { 53 | log.Infof("Task received: %s", taskID) 54 | 55 | // Extract task payload from the message parts 56 | var payload map[string]interface{} 57 | if len(message.Parts) > 0 { 58 | if textPart, ok := message.Parts[0].(protocol.TextPart); ok { 59 | if err := json.Unmarshal([]byte(textPart.Text), &payload); err != nil { 60 | log.Errorf("Failed to unmarshal payload text: %v", err) 61 | // Continue with empty payload 62 | payload = make(map[string]interface{}) 63 | } 64 | } 65 | } 66 | 67 | // Update status to working 68 | if err := handle.UpdateStatus(protocol.TaskStateWorking, &protocol.Message{ 69 | Role: protocol.MessageRoleAgent, 70 | Parts: []protocol.Part{ 71 | protocol.NewTextPart("Task queued for processing..."), 72 | }, 73 | }); err != nil { 74 | return fmt.Errorf("failed to update task status: %v", err) 75 | } 76 | 77 | // Start asynchronous processing 78 | go p.processTaskAsync(ctx, taskID, payload, handle) 79 | 80 | return nil 81 | } 82 | 83 | // OnTaskStatusUpdate implements the TaskProcessor interface. 84 | func (p *pushNotificationTaskProcessor) OnTaskStatusUpdate( 85 | ctx context.Context, 86 | taskID string, 87 | state protocol.TaskState, 88 | message *protocol.Message, 89 | ) error { 90 | log.Infof("Updating task status for task: %s with status: %s", taskID, state) 91 | if state == protocol.TaskStateCompleted || 92 | state == protocol.TaskStateFailed || state == protocol.TaskStateCanceled { 93 | p.manager.sendPushNotification(ctx, taskID, string(state)) 94 | } 95 | return nil 96 | } 97 | 98 | // processTaskAsync handles the actual task processing in a separate goroutine. 99 | func (p *pushNotificationTaskProcessor) processTaskAsync( 100 | ctx context.Context, 101 | taskID string, 102 | payload map[string]interface{}, 103 | handle taskmanager.TaskHandle, 104 | ) { 105 | log.Infof("Starting async processing of task: %s", taskID) 106 | 107 | // Process the task (simulating work) 108 | time.Sleep(5 * time.Second) // Longer processing time to demonstrate async behavior 109 | 110 | // Prepare message for completion 111 | completeMsg := "Task completed" 112 | if content, ok := payload["content"].(string); ok { 113 | completeMsg = fmt.Sprintf("Task completed: %s", content) 114 | } 115 | 116 | // Complete the task 117 | // When we call UpdateStatus with a terminal state (like completed), 118 | // the task manager automatically: 119 | // 1. Updates the task status in memory 120 | // 2. Sends a push notification to the registered webhook URL (if enabled) 121 | if err := handle.UpdateStatus(protocol.TaskStateCompleted, &protocol.Message{ 122 | Role: protocol.MessageRoleAgent, 123 | Parts: []protocol.Part{ 124 | protocol.NewTextPart(completeMsg), 125 | }, 126 | }); err != nil { 127 | log.Errorf("Failed to update task status: %v", err) 128 | return 129 | } 130 | 131 | log.Infof("Task completed asynchronously: %s", taskID) 132 | } 133 | 134 | type pushNotificationTaskManager struct { 135 | taskmanager.TaskManager 136 | authenticator *auth.PushNotificationAuthenticator 137 | } 138 | 139 | func (m *pushNotificationTaskManager) OnSendTask(ctx context.Context, request protocol.SendTaskParams) (*protocol.Task, error) { 140 | task, err := m.TaskManager.OnSendTask(ctx, request) 141 | if err != nil { 142 | return nil, err 143 | } 144 | return task, nil 145 | } 146 | 147 | func (m *pushNotificationTaskManager) sendPushNotification(ctx context.Context, taskID, status string) { 148 | log.Infof("Sending push notification for task: %s with status: %s", taskID, status) 149 | // Get push config from task manager 150 | ptm, ok := m.TaskManager.(*taskmanager.MemoryTaskManager) 151 | if !ok { 152 | log.Errorf("failed to cast task manager to memory task manager") 153 | return 154 | } 155 | pushConfig, exists := ptm.PushNotifications[taskID] 156 | if !exists { 157 | log.Infof("No push notification configuration for task: %s", taskID) 158 | return 159 | } 160 | 161 | // Send push notification 162 | if err := m.authenticator.SendPushNotification(ctx, pushConfig.URL, map[string]interface{}{ 163 | "task_id": taskID, 164 | "status": status, 165 | "timestamp": time.Now().Format(time.RFC3339), 166 | }); err != nil { 167 | log.Errorf("Failed to send push notification: %v", err) 168 | } else { 169 | log.Infof("Push notification sent successfully for task: %s", taskID) 170 | } 171 | } 172 | 173 | func main() { 174 | // Parse command line flags 175 | var ( 176 | port = flag.Int("port", defaultServerPort, "RPC port") 177 | notifyHost = flag.String("notify-host", defaultNotifyHost, "push notification host") 178 | ) 179 | flag.Parse() 180 | 181 | // Create agent card 182 | agentCard := server.AgentCard{ 183 | Name: "Push Notification Example", 184 | Description: strPtr("A2A server example with push notification support"), 185 | URL: fmt.Sprintf("http://localhost:%d/", *port), 186 | Version: "1.0.0", 187 | Capabilities: server.AgentCapabilities{ 188 | Streaming: true, 189 | PushNotifications: true, 190 | StateTransitionHistory: true, 191 | }, 192 | DefaultInputModes: []string{"text"}, 193 | DefaultOutputModes: []string{"text"}, 194 | } 195 | 196 | authenticator := auth.NewPushNotificationAuthenticator() 197 | if err := authenticator.GenerateKeyPair(); err != nil { 198 | log.Fatalf("failed to generate key pair: %v", err) 199 | } 200 | 201 | // Create task processor 202 | processor := &pushNotificationTaskProcessor{ 203 | notifyHost: *notifyHost, 204 | } 205 | // Create task manager 206 | tm, err := taskmanager.NewMemoryTaskManager(processor) 207 | if err != nil { 208 | log.Fatalf("failed to create task manager: %v", err) 209 | } 210 | 211 | // Create custom task manager with push notification support 212 | customTM := &pushNotificationTaskManager{ 213 | TaskManager: tm, 214 | authenticator: authenticator, 215 | } 216 | processor.manager = customTM 217 | // Combine standard options with additional options 218 | options := []server.Option{ 219 | server.WithJWKSEndpoint(true, "/.well-known/jwks.json"), 220 | server.WithPushNotificationAuthenticator(authenticator), 221 | } 222 | 223 | // Create server with the authenticator 224 | a2aServer, err := server.NewA2AServer( 225 | agentCard, 226 | customTM, 227 | options..., 228 | ) 229 | if err != nil { 230 | log.Fatalf("failed to create A2A server: %v", err) 231 | } 232 | 233 | // Start the server 234 | log.Infof("Starting A2A server on port %d...", *port) 235 | if err := a2aServer.Start(fmt.Sprintf(":%d", *port)); err != nil { 236 | log.Fatalf("Failed to start A2A server: %v", err) 237 | } 238 | } 239 | 240 | // Helper function to create string pointer 241 | func strPtr(s string) *string { 242 | return &s 243 | } 244 | -------------------------------------------------------------------------------- /examples/multi/README.md: -------------------------------------------------------------------------------- 1 | # Multi-Agent System Example 2 | 3 | This example demonstrates a multi-agent system built with the A2A framework. The system consists of a root agent that routes requests to specialized sub-agents based on the content of the requests. 4 | 5 | ## System Architecture 6 | 7 | The multi-agent system consists of: 8 | 9 | * A `Root Agent`: Listens on port 8080 and routes incoming requests to appropriate sub-agents based on content analysis. 10 | * Three specialized `Sub-Agents`: 11 | * `Exchange Agent`: Provides currency exchange information. Listens on port 8081. 12 | * `Creative Agent`: Handles creative writing requests (stories, poems, etc.). Listens on port 8082. 13 | * `Reimbursement Agent`: Processes expense reimbursement requests. Listens on port 8083. 14 | 15 | The system also includes a CLI client for interacting with the agents. 16 | 17 | ## Prerequisites 18 | 19 | * Go (version 1.21 or later recommended) 20 | * Google API key (Obtain for free from https://ai.google.dev/gemini-api/docs/api-key and set it as `GOOGLE_API_KEY` environment variable) 21 | 22 | ## Directory Structure 23 | 24 | ``` 25 | multi/ 26 | ├── go.mod # Module dependencies 27 | ├── go.sum # Dependency checksums 28 | ├── README.md # This documentation 29 | ├── root/ # Root agent implementation 30 | │ └── main.go 31 | ├── creative/ # Creative writing agent 32 | │ ├── main.go 33 | │ └── test.sh # Test script 34 | ├── exchange/ # Currency exchange agent 35 | │ ├── main.go 36 | │ └── test.sh # Test script 37 | ├── reimbursement/ # Reimbursement processing agent 38 | │ ├── main.go 39 | │ └── test.sh # Test script 40 | └── cli/ # Command-line interface 41 | └── main.go 42 | ``` 43 | 44 | ## Building and Running the System 45 | 46 | ### Building the Agents 47 | 48 | Navigate to the example directory and build each component: 49 | 50 | ```bash 51 | cd a2a-go-github/examples/multi 52 | 53 | # Build the root agent 54 | go build -o root/root ./root 55 | 56 | # Build the sub-agents 57 | go build -o creative/creative ./creative 58 | go build -o exchange/exchange ./exchange 59 | go build -o reimbursement/reimbursement ./reimbursement 60 | 61 | # Build the CLI client 62 | go build -o cli/cli ./cli 63 | ``` 64 | 65 | ### Running the Agents 66 | 67 | Run each agent in a separate terminal window: 68 | 69 | #### Terminal 1: Exchange Agent 70 | ```bash 71 | cd a2a-go-github/examples/multi 72 | ./exchange/exchange -port 8081 73 | ``` 74 | 75 | #### Terminal 2: Creative Agent 76 | ```bash 77 | cd a2a-go-github/examples/multi 78 | ./creative/creative -port 8082 79 | ``` 80 | 81 | #### Terminal 3: Reimbursement Agent 82 | ```bash 83 | cd a2a-go-github/examples/multi 84 | ./reimbursement/reimbursement -port 8083 85 | ``` 86 | 87 | #### Terminal 4: Root Agent 88 | ```bash 89 | cd a2a-go-github/examples/multi 90 | ./root/root -port 8080 91 | ``` 92 | 93 | Remember to set the `GOOGLE_API_KEY` environment variable to use Gemini model: 94 | 95 | ```bash 96 | export GOOGLE_API_KEY=your_api_key 97 | ./root/root -port 8080 98 | ``` 99 | 100 | ### Running in Background (Alternative) 101 | 102 | Alternatively, you can run all agents in the background: 103 | 104 | ```bash 105 | cd a2a-go-github/examples/multi 106 | ./exchange/exchange -port 8081 & 107 | ./creative/creative -port 8082 & 108 | ./reimbursement/reimbursement -port 8083 & 109 | ./root/root -port 8080 & 110 | ``` 111 | 112 | ## Using the CLI to Interact with the System 113 | 114 | Once all agents are running, use the CLI client to interact with the system: 115 | 116 | ```bash 117 | cd a2a-go-github/examples/multi 118 | ./cli/cli 119 | ``` 120 | 121 | This will connect to the root agent at http://localhost:8080 by default. You can specify a different URL with the `-url` flag: 122 | 123 | ```bash 124 | ./cli/cli -url http://localhost:8080 125 | ``` 126 | 127 | After starting the CLI, you'll see a prompt where you can type requests: 128 | 129 | ``` 130 | Connected to root agent at http://localhost:8080 131 | Type your requests and press Enter. Type 'exit' to quit. 132 | > 133 | ``` 134 | 135 | ## Example Interactions 136 | 137 | Try these example interactions: 138 | 139 | ### Creative Writing Requests 140 | ``` 141 | > Write a short poem about autumn 142 | > Create a story about a space adventure 143 | ``` 144 | 145 | ### Currency Exchange Requests 146 | ``` 147 | > What's the exchange rate from USD to EUR? 148 | > Convert 100 USD to JPY 149 | ``` 150 | 151 | ### Reimbursement Requests 152 | ``` 153 | > I need to get reimbursed for a $50 business lunch 154 | > Submit a receipt for my office supplies purchase 155 | ``` 156 | 157 | The root agent will analyze your request and route it to the appropriate specialized agent, then return the response. 158 | 159 | ## How It Works 160 | 161 | 1. The CLI client sends your request to the root agent. 162 | 2. The root agent analyzes the content of your request to determine which specialized agent should handle it. 163 | 3. The root agent forwards your request to the appropriate sub-agent. 164 | 4. The sub-agent processes your request and sends back a response. 165 | 5. The root agent returns this response to the CLI client. 166 | 6. The CLI displays the response. 167 | 168 | ## Stopping the Agents 169 | 170 | If you ran the agents in separate terminal windows, use Ctrl+C to stop each one. 171 | 172 | If you ran them in the background, find and kill the processes: 173 | 174 | ```bash 175 | # Find the PIDs 176 | pgrep -f "root|creative|exchange|reimbursement" 177 | 178 | # Kill the processes 179 | kill $(pgrep -f "root|creative|exchange|reimbursement") 180 | ``` 181 | -------------------------------------------------------------------------------- /examples/multi/cli/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "context" 6 | "flag" 7 | "fmt" 8 | "os" 9 | "strings" 10 | 11 | "trpc.group/trpc-go/trpc-a2a-go/client" 12 | "trpc.group/trpc-go/trpc-a2a-go/log" 13 | "trpc.group/trpc-go/trpc-a2a-go/protocol" 14 | ) 15 | 16 | func main() { 17 | // Define command-line flags - only root agent is supported 18 | rootAgentURL := flag.String("url", "http://localhost:8080", "URL for the root agent") 19 | flag.Parse() 20 | 21 | // Create the A2A client 22 | a2aClient, err := client.NewA2AClient(*rootAgentURL) 23 | if err != nil { 24 | log.Fatal("Failed to create A2A client: %v", err) 25 | } 26 | 27 | fmt.Printf("Connected to root agent at %s\n", *rootAgentURL) 28 | fmt.Println("Type your requests and press Enter. Type 'exit' to quit.") 29 | 30 | // Create a scanner to read user input 31 | scanner := bufio.NewScanner(os.Stdin) 32 | 33 | // Main input loop 34 | for { 35 | fmt.Print("> ") 36 | if !scanner.Scan() { 37 | break 38 | } 39 | 40 | input := scanner.Text() 41 | if strings.ToLower(input) == "exit" { 42 | break 43 | } 44 | 45 | if input == "" { 46 | continue 47 | } 48 | 49 | // Send the task to the agent 50 | response, err := sendTask(a2aClient, input) 51 | if err != nil { 52 | fmt.Printf("Error: %v\n", err) 53 | continue 54 | } 55 | 56 | // Display the response 57 | fmt.Printf("\nResponse: %s\n\n", response) 58 | } 59 | 60 | if err := scanner.Err(); err != nil { 61 | fmt.Fprintf(os.Stderr, "Error reading input: %v\n", err) 62 | } 63 | } 64 | 65 | // sendTask sends a task to the agent and waits for the response. 66 | func sendTask(client *client.A2AClient, text string) (string, error) { 67 | ctx := context.Background() 68 | 69 | // Create the task parameters with the user's message 70 | params := protocol.SendTaskParams{ 71 | Message: protocol.Message{ 72 | Role: protocol.MessageRoleUser, 73 | Parts: []protocol.Part{ 74 | protocol.NewTextPart(text), 75 | }, 76 | }, 77 | } 78 | 79 | // Send the task to the agent 80 | task, err := client.SendTasks(ctx, params) 81 | if err != nil { 82 | return "", fmt.Errorf("failed to send task: %w", err) 83 | } 84 | 85 | // Extract the response text from the task status message 86 | if task.Status.Message == nil { 87 | return "", fmt.Errorf("no response message from agent") 88 | } 89 | 90 | // Extract text from the response message 91 | return extractText(*task.Status.Message), nil 92 | } 93 | 94 | // extractText extracts the text content from a message. 95 | func extractText(message protocol.Message) string { 96 | for _, part := range message.Parts { 97 | if textPart, ok := part.(protocol.TextPart); ok { 98 | return textPart.Text 99 | } 100 | } 101 | return "" 102 | } 103 | -------------------------------------------------------------------------------- /examples/multi/creative/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "flag" 6 | "fmt" 7 | "os" 8 | "os/signal" 9 | "strings" 10 | "syscall" 11 | 12 | "github.com/tmc/langchaingo/llms" 13 | "github.com/tmc/langchaingo/llms/googleai" 14 | "trpc.group/trpc-go/trpc-a2a-go/log" 15 | "trpc.group/trpc-go/trpc-a2a-go/protocol" 16 | "trpc.group/trpc-go/trpc-a2a-go/server" 17 | "trpc.group/trpc-go/trpc-a2a-go/taskmanager" 18 | ) 19 | 20 | // conversationCache to store conversation histories 21 | type conversationCache struct { 22 | conversations map[string][]string // maps sessionID -> message history 23 | } 24 | 25 | // newConversationCache creates a new conversation cache 26 | func newConversationCache() *conversationCache { 27 | return &conversationCache{ 28 | conversations: make(map[string][]string), 29 | } 30 | } 31 | 32 | // AddMessage adds a message to the conversation history 33 | func (c *conversationCache) AddMessage(sessionID string, message string) { 34 | if _, ok := c.conversations[sessionID]; !ok { 35 | c.conversations[sessionID] = make([]string, 0) 36 | } 37 | c.conversations[sessionID] = append(c.conversations[sessionID], message) 38 | if len(c.conversations[sessionID]) > 10 { // limit history to 10 messages 39 | c.conversations[sessionID] = c.conversations[sessionID][len(c.conversations[sessionID])-10:] 40 | } 41 | } 42 | 43 | // GetHistory retrieves the conversation history 44 | func (c *conversationCache) GetHistory(sessionID string) []string { 45 | if history, ok := c.conversations[sessionID]; ok { 46 | return history 47 | } 48 | return []string{} 49 | } 50 | 51 | // creativeWritingProcessor implements the taskmanager.TaskProcessor interface 52 | type creativeWritingProcessor struct { 53 | llm llms.Model 54 | cache *conversationCache 55 | } 56 | 57 | // newCreativeWritingProcessor creates a new creative writing processor 58 | func newCreativeWritingProcessor() (*creativeWritingProcessor, error) { 59 | // Initialize Google Gemini model 60 | llm, err := googleai.New( 61 | context.Background(), 62 | googleai.WithAPIKey(getAPIKey()), 63 | googleai.WithDefaultModel("gemini-1.5-flash"), 64 | ) 65 | if err != nil { 66 | return nil, fmt.Errorf("failed to initialize Gemini model: %w", err) 67 | } 68 | 69 | return &creativeWritingProcessor{ 70 | llm: llm, 71 | cache: newConversationCache(), 72 | }, nil 73 | } 74 | 75 | func getAPIKey() string { 76 | apiKey := os.Getenv("GOOGLE_API_KEY") 77 | if apiKey == "" { 78 | log.Warn("GOOGLE_API_KEY environment variable not set.") 79 | } 80 | return apiKey 81 | } 82 | 83 | // Process implements the taskmanager.TaskProcessor interface 84 | func (p *creativeWritingProcessor) Process( 85 | ctx context.Context, 86 | taskID string, 87 | message protocol.Message, 88 | handle taskmanager.TaskHandle, 89 | ) error { 90 | // Extract text from the incoming message 91 | prompt := extractText(message) 92 | if prompt == "" { 93 | errMsg := "input message must contain text." 94 | log.Error("Task %s failed: %s", taskID, errMsg) 95 | 96 | // Update status to Failed via handle 97 | failedMessage := protocol.NewMessage( 98 | protocol.MessageRoleAgent, 99 | []protocol.Part{protocol.NewTextPart(errMsg)}, 100 | ) 101 | _ = handle.UpdateStatus(protocol.TaskStateFailed, &failedMessage) 102 | return fmt.Errorf(errMsg) 103 | } 104 | 105 | log.Info("Processing creative writing task %s with prompt: %s", taskID, prompt) 106 | 107 | // Get session ID from task metadata or use taskID as fallback 108 | sessionID := taskID 109 | 110 | // Update to in-progress status 111 | progressMessage := protocol.NewMessage( 112 | protocol.MessageRoleAgent, 113 | []protocol.Part{protocol.NewTextPart("Crafting your creative response...")}, 114 | ) 115 | if err := handle.UpdateStatus(protocol.TaskStateWorking, &progressMessage); err != nil { 116 | log.Error("Failed to update task status: %v", err) 117 | } 118 | 119 | // Build the context from conversation history 120 | history := p.cache.GetHistory(sessionID) 121 | 122 | var fullPrompt string 123 | if len(history) > 0 { 124 | // If we have conversation history, include it for context 125 | historyText := strings.Join(history, "\n\n") 126 | fullPrompt = fmt.Sprintf("Previous conversation:\n%s\n\nNew request: %s", historyText, prompt) 127 | } else { 128 | fullPrompt = prompt 129 | } 130 | 131 | // Add creative writing instructions 132 | systemPrompt := "You are a creative writing assistant. Your task is to provide creative, " + 133 | "engaging responses to the user's prompts. Use vivid language, imaginative scenarios, " + 134 | "and interesting characters when appropriate. If the user asks for a specific style or format " + 135 | "(poem, story, joke, etc.), follow their request." 136 | finalPrompt := fmt.Sprintf("%s\n\n%s", systemPrompt, fullPrompt) 137 | 138 | // Generate the creative response using the LLM 139 | response, err := llms.GenerateFromSinglePrompt(ctx, p.llm, finalPrompt) 140 | if err != nil { 141 | errorMsg := fmt.Sprintf("Failed to generate response: %v", err) 142 | log.Error("Task %s failed: %s", taskID, errorMsg) 143 | 144 | errorMessage := protocol.NewMessage( 145 | protocol.MessageRoleAgent, 146 | []protocol.Part{protocol.NewTextPart(errorMsg)}, 147 | ) 148 | return handle.UpdateStatus(protocol.TaskStateFailed, &errorMessage) 149 | } 150 | 151 | // Save prompt and response to conversation history 152 | p.cache.AddMessage(sessionID, fmt.Sprintf("User: %s", prompt)) 153 | p.cache.AddMessage(sessionID, fmt.Sprintf("Assistant: %s", response)) 154 | 155 | // Create response message with the generated text 156 | responseMessage := protocol.NewMessage( 157 | protocol.MessageRoleAgent, 158 | []protocol.Part{protocol.NewTextPart(response)}, 159 | ) 160 | 161 | // Update task status to completed 162 | if err := handle.UpdateStatus(protocol.TaskStateCompleted, &responseMessage); err != nil { 163 | return fmt.Errorf("failed to update task status: %w", err) 164 | } 165 | 166 | // Add response as an artifact 167 | artifact := protocol.Artifact{ 168 | Name: stringPtr("Creative Writing Response"), 169 | Description: stringPtr(prompt), 170 | Index: 0, 171 | Parts: []protocol.Part{protocol.NewTextPart(response)}, 172 | LastChunk: boolPtr(true), 173 | } 174 | 175 | if err := handle.AddArtifact(artifact); err != nil { 176 | log.Error("Error adding artifact for task %s: %v", taskID, err) 177 | } 178 | 179 | return nil 180 | } 181 | 182 | // extractText extracts the text content from a message 183 | func extractText(message protocol.Message) string { 184 | for _, part := range message.Parts { 185 | if textPart, ok := part.(protocol.TextPart); ok { 186 | return textPart.Text 187 | } 188 | } 189 | return "" 190 | } 191 | 192 | // Helper functions 193 | func stringPtr(s string) *string { 194 | return &s 195 | } 196 | 197 | func boolPtr(b bool) *bool { 198 | return &b 199 | } 200 | 201 | // getAgentCard returns the agent's metadata 202 | func getAgentCard() server.AgentCard { 203 | return server.AgentCard{ 204 | Name: "Creative Writing Agent", 205 | Description: stringPtr("An agent that generates creative writing based on prompts using Google Gemini."), 206 | URL: "http://localhost:8082", 207 | Version: "1.0.0", 208 | Capabilities: server.AgentCapabilities{ 209 | Streaming: false, 210 | PushNotifications: false, 211 | StateTransitionHistory: true, 212 | }, 213 | DefaultInputModes: []string{string(protocol.PartTypeText)}, 214 | DefaultOutputModes: []string{string(protocol.PartTypeText)}, 215 | Skills: []server.AgentSkill{ 216 | { 217 | ID: "creative_writing", 218 | Name: "Creative Writing", 219 | Description: stringPtr("Creates engaging creative text based on user prompts."), 220 | Examples: []string{ 221 | "Write a short story about a space explorer", 222 | "Compose a poem about autumn leaves", 223 | "Create a funny dialogue between a cat and a dog", 224 | "Write a brief fantasy adventure about a magical forest", 225 | }, 226 | }, 227 | }, 228 | } 229 | } 230 | 231 | func main() { 232 | // Parse command-line flags 233 | host := flag.String("host", "localhost", "Host to listen on") 234 | port := flag.Int("port", 8082, "Port to listen on for the creative writing agent") 235 | flag.Parse() 236 | 237 | // Create the creative writing processor 238 | processor, err := newCreativeWritingProcessor() 239 | if err != nil { 240 | log.Fatal("Failed to create creative writing processor: %v", err) 241 | } 242 | 243 | // Create task manager and inject processor 244 | taskManager, err := taskmanager.NewMemoryTaskManager(processor) 245 | if err != nil { 246 | log.Fatal("Failed to create task manager: %v", err) 247 | } 248 | 249 | // Create the A2A server 250 | agentCard := getAgentCard() 251 | a2aServer, err := server.NewA2AServer(agentCard, taskManager) 252 | if err != nil { 253 | log.Fatal("Failed to create A2A server: %v", err) 254 | } 255 | 256 | // Set up a channel to listen for termination signals 257 | sigChan := make(chan os.Signal, 1) 258 | signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) 259 | 260 | // Start the server in a goroutine 261 | go func() { 262 | serverAddr := fmt.Sprintf("%s:%d", *host, *port) 263 | log.Info("Starting Creative Writing Agent server on %s", serverAddr) 264 | if err := a2aServer.Start(serverAddr); err != nil { 265 | log.Fatal("Server failed: %v", err) 266 | } 267 | }() 268 | 269 | // Wait for termination signal 270 | sig := <-sigChan 271 | log.Info("Received signal %v, shutting down...", sig) 272 | } 273 | -------------------------------------------------------------------------------- /examples/multi/creative/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # ==================================================================== 4 | # Creative Writing Agent Demo Script 5 | # ==================================================================== 6 | # This script demonstrates how to use the Creative Writing Agent API 7 | # with examples of different request types. 8 | # ==================================================================== 9 | 10 | # Terminal colors for better readability 11 | BLUE='\033[0;34m' 12 | GREEN='\033[0;32m' 13 | YELLOW='\033[1;33m' 14 | RED='\033[0;31m' 15 | NC='\033[0m' # No Color 16 | 17 | # Check for required environment variable 18 | if [ -z "$GOOGLE_API_KEY" ]; then 19 | echo -e "${RED}Error: GOOGLE_API_KEY environment variable is not set.${NC}" 20 | echo -e "Please set it with: ${YELLOW}export GOOGLE_API_KEY=your_api_key${NC}" 21 | exit 1 22 | fi 23 | 24 | # Function to make API calls and format responses 25 | call_api() { 26 | local request_name=$1 27 | local request_data=$2 28 | local request_id=$3 29 | 30 | echo -e "\n${BLUE}======================================================${NC}" 31 | echo -e "${BLUE}Example $request_id: $request_name${NC}" 32 | echo -e "${BLUE}======================================================${NC}" 33 | echo -e "${YELLOW}Request:${NC}" 34 | echo $request_data | jq . 35 | echo -e "${GREEN}Response:${NC}" 36 | 37 | # Make the API call and capture the response 38 | response=$(curl -s -H "Content-Type: application/json" -X POST http://127.0.0.1:8082 -d "$request_data") 39 | 40 | # Pretty print the response with jq if available 41 | if command -v jq &> /dev/null; then 42 | echo $response | jq . 43 | else 44 | echo $response 45 | fi 46 | 47 | echo "" 48 | } 49 | 50 | echo -e "${GREEN}=====================================================================${NC}" 51 | echo -e "${GREEN} Creative Writing Agent Demo ${NC}" 52 | echo -e "${GREEN}=====================================================================${NC}" 53 | echo -e "This script demonstrates different ways to interact with the Creative Writing Agent API.\n" 54 | 55 | # Example 1: Generate a short story 56 | echo -e "${YELLOW}Example 1: Generating a short story${NC}" 57 | echo -e "This request asks the agent to write a short story about a robot learning emotions.\n" 58 | 59 | STORY_REQUEST='{ 60 | "jsonrpc": "2.0", 61 | "method": "tasks/send", 62 | "id": 1, 63 | "params": { 64 | "id": "story-1", 65 | "message": { 66 | "role": "user", 67 | "parts": [ 68 | { 69 | "type": "text", 70 | "text": "Write a short story about a robot that learns to feel emotions" 71 | } 72 | ] 73 | } 74 | } 75 | }' 76 | 77 | call_api "Generate a short story" "$STORY_REQUEST" "1" 78 | STORY_ID="story-1" 79 | 80 | # Wait for processing 81 | echo -e "${YELLOW}Waiting for the first request to complete...${NC}" 82 | sleep 5 83 | 84 | # Example 2: Follow-up request in the same conversation 85 | echo -e "${YELLOW}Example 2: Follow-up request in the same conversation${NC}" 86 | echo -e "This request builds on the previous story by asking for a poem about the same robot.\nNote how we use the same sessionId to maintain context.\n" 87 | 88 | POEM_REQUEST='{ 89 | "jsonrpc": "2.0", 90 | "method": "tasks/send", 91 | "id": 2, 92 | "params": { 93 | "id": "poem-1", 94 | "sessionId": "story-1", 95 | "message": { 96 | "role": "user", 97 | "parts": [ 98 | { 99 | "type": "text", 100 | "text": "Now write a poem about this robot" 101 | } 102 | ] 103 | } 104 | } 105 | }' 106 | 107 | call_api "Follow-up poem request" "$POEM_REQUEST" "2" 108 | 109 | # Example 3: New conversation with a different style 110 | echo -e "${YELLOW}Example 3: New conversation with a different creative style${NC}" 111 | echo -e "This starts a new conversation with a request for dialogue writing.\n" 112 | 113 | DIALOGUE_REQUEST='{ 114 | "jsonrpc": "2.0", 115 | "method": "tasks/send", 116 | "id": 3, 117 | "params": { 118 | "id": "dialogue-1", 119 | "message": { 120 | "role": "user", 121 | "parts": [ 122 | { 123 | "type": "text", 124 | "text": "Create a funny dialogue between a cat and a dog discussing who is better" 125 | } 126 | ] 127 | } 128 | } 129 | }' 130 | 131 | call_api "Dialogue writing" "$DIALOGUE_REQUEST" "3" 132 | 133 | # Example 4: Retrieving task status 134 | echo -e "${YELLOW}Example 4: Retrieving task status and content${NC}" 135 | echo -e "This example shows how to retrieve a completed task by its ID.\n" 136 | 137 | GET_REQUEST='{ 138 | "jsonrpc": "2.0", 139 | "method": "tasks/get", 140 | "id": 4, 141 | "params": { 142 | "id": "story-1" 143 | } 144 | }' 145 | 146 | call_api "Get task status" "$GET_REQUEST" "4" 147 | 148 | echo -e "\n${GREEN}=====================================================================${NC}" 149 | echo -e "${GREEN} Demo Complete ${NC}" 150 | echo -e "${GREEN}=====================================================================${NC}" 151 | echo -e "${YELLOW}Usage Tips:${NC}" 152 | echo -e "1. Use the same sessionId for follow-up questions to maintain context" 153 | echo -e "2. Request different creative formats: stories, poems, dialogues, etc." 154 | echo -e "3. Retrieve task content with the tasks/get method" 155 | echo -e "4. Run the Creative Writing Agent with: go run main.go\n" 156 | -------------------------------------------------------------------------------- /examples/multi/exchange/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # ==================================================================== 4 | # Currency Exchange Agent Demo Script 5 | # ==================================================================== 6 | # This script demonstrates how to use the Currency Exchange Agent API 7 | # with examples of different request types. 8 | # ==================================================================== 9 | 10 | # Terminal colors for better readability 11 | BLUE='\033[0;34m' 12 | GREEN='\033[0;32m' 13 | YELLOW='\033[1;33m' 14 | RED='\033[0;31m' 15 | NC='\033[0m' # No Color 16 | 17 | # Function to make API calls and format responses 18 | call_api() { 19 | local request_name=$1 20 | local request_data=$2 21 | local request_id=$3 22 | 23 | echo -e "\n${BLUE}======================================================${NC}" 24 | echo -e "${BLUE}Example $request_id: $request_name${NC}" 25 | echo -e "${BLUE}======================================================${NC}" 26 | echo -e "${YELLOW}Request:${NC}" 27 | echo $request_data | jq . 2>/dev/null || echo $request_data 28 | echo -e "${GREEN}Response:${NC}" 29 | 30 | # Make the API call and capture the response 31 | response=$(curl -s -H "Content-Type: application/json" -X POST http://127.0.0.1:8081 -d "$request_data") 32 | 33 | # Pretty print the response with jq if available 34 | if command -v jq &> /dev/null; then 35 | echo $response | jq . 36 | else 37 | echo $response 38 | fi 39 | 40 | echo "" 41 | # Return the response for potential processing 42 | echo "$response" 43 | } 44 | 45 | echo -e "${GREEN}=====================================================================${NC}" 46 | echo -e "${GREEN} Currency Exchange Agent Demo ${NC}" 47 | echo -e "${GREEN}=====================================================================${NC}" 48 | echo -e "This script demonstrates different ways to interact with the Currency Exchange Agent API.\n" 49 | 50 | # Example 1: Basic currency exchange 51 | echo -e "${YELLOW}Example 1: Basic currency exchange conversion${NC}" 52 | echo -e "This request converts CNY to USD on a specific date.\n" 53 | 54 | BASIC_EXCHANGE='{ 55 | "jsonrpc": "2.0", 56 | "method": "tasks/send", 57 | "id": 1, 58 | "params": { 59 | "id": "test-exchange-1", 60 | "message": { 61 | "role": "user", 62 | "parts": [ 63 | { 64 | "type": "text", 65 | "text": "convert cny to usd on date 2025-04-16" 66 | } 67 | ] 68 | } 69 | } 70 | }' 71 | 72 | call_api "Basic currency exchange" "$BASIC_EXCHANGE" "1" 73 | 74 | # Wait for processing 75 | echo -e "${YELLOW}Waiting for the first request to complete...${NC}" 76 | sleep 2 77 | 78 | # Example 2: More complex exchange rate query 79 | echo -e "${YELLOW}Example 2: More complex exchange rate query${NC}" 80 | echo -e "This request asks for the exchange rate between EUR and JPY.\n" 81 | 82 | COMPLEX_EXCHANGE='{ 83 | "jsonrpc": "2.0", 84 | "method": "tasks/send", 85 | "id": 2, 86 | "params": { 87 | "id": "test-exchange-2", 88 | "message": { 89 | "role": "user", 90 | "parts": [ 91 | { 92 | "type": "text", 93 | "text": "What is the exchange rate from EUR to JPY?" 94 | } 95 | ] 96 | } 97 | } 98 | }' 99 | 100 | call_api "Complex exchange rate query" "$COMPLEX_EXCHANGE" "2" 101 | 102 | # Example 3: Historical exchange rate query 103 | echo -e "${YELLOW}Example 3: Historical exchange rate query${NC}" 104 | echo -e "This request asks for a historical exchange rate.\n" 105 | 106 | HISTORICAL_EXCHANGE='{ 107 | "jsonrpc": "2.0", 108 | "method": "tasks/send", 109 | "id": 3, 110 | "params": { 111 | "id": "test-exchange-3", 112 | "message": { 113 | "role": "user", 114 | "parts": [ 115 | { 116 | "type": "text", 117 | "text": "What was the exchange rate from USD to GBP on January 1, 2023?" 118 | } 119 | ] 120 | } 121 | } 122 | }' 123 | 124 | call_api "Historical exchange rate query" "$HISTORICAL_EXCHANGE" "3" 125 | 126 | echo -e "\n${GREEN}=====================================================================${NC}" 127 | echo -e "${GREEN} Demo Complete ${NC}" 128 | echo -e "${GREEN}=====================================================================${NC}" 129 | echo -e "${YELLOW}Usage Tips:${NC}" 130 | echo -e "1. Specify currency codes (USD, EUR, GBP, etc.) in your requests" 131 | echo -e "2. Include dates for historical exchange rates" 132 | echo -e "3. You can ask about trends or comparative rates between currencies" 133 | echo -e "4. Run the Currency Exchange Agent with: go run main.go\n" 134 | -------------------------------------------------------------------------------- /examples/multi/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/futrime/a2a-go-github/examples/multi 2 | 3 | go 1.23.0 4 | 5 | toolchain go1.23.7 6 | 7 | require ( 8 | github.com/tmc/langchaingo v0.1.13 9 | trpc.group/trpc-go/trpc-a2a-go v0.0.0 10 | ) 11 | 12 | require ( 13 | cloud.google.com/go v0.114.0 // indirect 14 | cloud.google.com/go/ai v0.7.0 // indirect 15 | cloud.google.com/go/aiplatform v1.68.0 // indirect 16 | cloud.google.com/go/auth v0.5.1 // indirect 17 | cloud.google.com/go/auth/oauth2adapt v0.2.2 // indirect 18 | cloud.google.com/go/compute/metadata v0.3.0 // indirect 19 | cloud.google.com/go/iam v1.1.8 // indirect 20 | cloud.google.com/go/longrunning v0.5.7 // indirect 21 | cloud.google.com/go/vertexai v0.12.0 // indirect 22 | github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect 23 | github.com/dlclark/regexp2 v1.10.0 // indirect 24 | github.com/felixge/httpsnoop v1.0.4 // indirect 25 | github.com/go-logr/logr v1.4.1 // indirect 26 | github.com/go-logr/stdr v1.2.2 // indirect 27 | github.com/goccy/go-json v0.10.3 // indirect 28 | github.com/golang-jwt/jwt/v5 v5.2.2 // indirect 29 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect 30 | github.com/golang/protobuf v1.5.4 // indirect 31 | github.com/google/generative-ai-go v0.15.1 // indirect 32 | github.com/google/s2a-go v0.1.7 // indirect 33 | github.com/google/uuid v1.6.0 // indirect 34 | github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect 35 | github.com/googleapis/gax-go/v2 v2.12.4 // indirect 36 | github.com/lestrrat-go/blackmagic v1.0.2 // indirect 37 | github.com/lestrrat-go/httpcc v1.0.1 // indirect 38 | github.com/lestrrat-go/httprc v1.0.6 // indirect 39 | github.com/lestrrat-go/iter v1.0.2 // indirect 40 | github.com/lestrrat-go/jwx/v2 v2.1.4 // indirect 41 | github.com/lestrrat-go/option v1.0.1 // indirect 42 | github.com/pkoukk/tiktoken-go v0.1.7 // indirect 43 | github.com/segmentio/asm v1.2.0 // indirect 44 | go.opencensus.io v0.24.0 // indirect 45 | go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.51.0 // indirect 46 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.51.0 // indirect 47 | go.opentelemetry.io/otel v1.26.0 // indirect 48 | go.opentelemetry.io/otel/metric v1.26.0 // indirect 49 | go.opentelemetry.io/otel/trace v1.26.0 // indirect 50 | go.uber.org/multierr v1.10.0 // indirect 51 | go.uber.org/zap v1.27.0 // indirect 52 | golang.org/x/crypto v0.35.0 // indirect 53 | golang.org/x/net v0.26.0 // indirect 54 | golang.org/x/oauth2 v0.29.0 // indirect 55 | golang.org/x/sync v0.11.0 // indirect 56 | golang.org/x/sys v0.30.0 // indirect 57 | golang.org/x/text v0.22.0 // indirect 58 | golang.org/x/time v0.5.0 // indirect 59 | google.golang.org/api v0.183.0 // indirect 60 | google.golang.org/genproto v0.0.0-20240528184218-531527333157 // indirect 61 | google.golang.org/genproto/googleapis/api v0.0.0-20240604185151-ef581f913117 // indirect 62 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240604185151-ef581f913117 // indirect 63 | google.golang.org/grpc v1.64.1 // indirect 64 | google.golang.org/protobuf v1.34.1 // indirect 65 | ) 66 | 67 | replace trpc.group/trpc-go/trpc-a2a-go => ../.. 68 | -------------------------------------------------------------------------------- /examples/multi/reimbursement/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # ==================================================================== 4 | # Reimbursement Agent Demo Script 5 | # ==================================================================== 6 | # This script demonstrates how to use the Reimbursement Agent API 7 | # with examples of different request types. 8 | # ==================================================================== 9 | 10 | # Terminal colors for better readability 11 | BLUE='\033[0;34m' 12 | GREEN='\033[0;32m' 13 | YELLOW='\033[1;33m' 14 | RED='\033[0;31m' 15 | NC='\033[0m' # No Color 16 | 17 | # Check for required environment variable 18 | if [ -z "$GOOGLE_API_KEY" ]; then 19 | echo -e "${RED}Error: GOOGLE_API_KEY environment variable is not set.${NC}" 20 | echo -e "Please set it with: ${YELLOW}export GOOGLE_API_KEY=your_api_key${NC}" 21 | exit 1 22 | fi 23 | 24 | # Function to extract request_id from JSON response 25 | extract_request_id() { 26 | local json="$1" 27 | # Remove newlines and handle escaped quotes for better matching 28 | local cleaned_json=$(echo "$json" | tr -d '\n' | sed 's/\\"/"/g') 29 | # Try multiple patterns to increase chances of matching 30 | local request_id=$(echo "$cleaned_json" | grep -o '"request_id": *"request_id_[0-9]*"' | grep -o 'request_id_[0-9]*' | head -1) 31 | 32 | if [ -z "$request_id" ]; then 33 | # Try alternate pattern with different quote formatting 34 | request_id=$(echo "$cleaned_json" | grep -o '"request_id":"request_id_[0-9]*"' | grep -o 'request_id_[0-9]*' | head -1) 35 | fi 36 | 37 | echo "$request_id" 38 | } 39 | 40 | # Function to make API calls and format responses 41 | call_api() { 42 | local request_name=$1 43 | local request_data=$2 44 | local request_id=$3 45 | 46 | echo -e "\n${BLUE}======================================================${NC}" 47 | echo -e "${BLUE}Example $request_id: $request_name${NC}" 48 | echo -e "${BLUE}======================================================${NC}" 49 | echo -e "${YELLOW}Request:${NC}" 50 | echo $request_data | jq . 2>/dev/null || echo $request_data 51 | echo -e "${GREEN}Response:${NC}" 52 | 53 | # Make the API call and capture the response 54 | response=$(curl -s -H "Content-Type: application/json" -X POST http://127.0.0.1:8083 -d "$request_data") 55 | 56 | # Pretty print the response with jq if available 57 | if command -v jq &> /dev/null; then 58 | echo $response | jq . 59 | else 60 | echo $response 61 | fi 62 | 63 | echo "" 64 | # Return the response for potential processing 65 | echo "$response" 66 | } 67 | 68 | echo -e "${GREEN}=====================================================================${NC}" 69 | echo -e "${GREEN} Reimbursement Agent Demo ${NC}" 70 | echo -e "${GREEN}=====================================================================${NC}" 71 | echo -e "This script demonstrates different ways to interact with the Reimbursement Agent API.\n" 72 | 73 | # Example 1: New reimbursement request 74 | echo -e "${YELLOW}Example 1: Creating a new reimbursement request${NC}" 75 | echo -e "This request initiates a new reimbursement process and returns a form.\n" 76 | 77 | NEW_REQUEST='{ 78 | "jsonrpc": "2.0", 79 | "method": "tasks/send", 80 | "id": 1, 81 | "params": { 82 | "id": "test-reimburse-1", 83 | "message": { 84 | "role": "user", 85 | "parts": [ 86 | { 87 | "type": "text", 88 | "text": "I need to get reimbursed for $50 spent on office supplies on 2023-11-15." 89 | } 90 | ] 91 | } 92 | } 93 | }' 94 | 95 | RESPONSE=$(call_api "Create reimbursement request" "$NEW_REQUEST" "1") 96 | 97 | # Extract request_id using the function 98 | REQUEST_ID=$(extract_request_id "$RESPONSE") 99 | if [ -z "$REQUEST_ID" ]; then 100 | echo -e "${YELLOW}Couldn't extract request_id from response. Using fallback ID.${NC}" 101 | # Use a fallback ID since we can't extract it 102 | REQUEST_ID="request_id_$(date +%s)" 103 | else 104 | echo -e "${GREEN}Extracted request_id: $REQUEST_ID${NC}" 105 | fi 106 | 107 | # Wait for processing 108 | echo -e "${YELLOW}Waiting for the first request to complete...${NC}" 109 | sleep 2 110 | 111 | # Example 2: Form submission with extracted request_id 112 | echo -e "${YELLOW}Example 2: Submitting a reimbursement form${NC}" 113 | echo -e "This request submits the completed form with the request_id from the first response.\n" 114 | 115 | FORM_SUBMIT='{ 116 | "jsonrpc": "2.0", 117 | "method": "tasks/send", 118 | "id": 2, 119 | "params": { 120 | "id": "test-reimburse-2", 121 | "sessionId": "test-reimburse-1", 122 | "message": { 123 | "role": "user", 124 | "parts": [ 125 | { 126 | "type": "text", 127 | "text": "{\"request_id\":\"'"$REQUEST_ID"'\",\"date\":\"2023-11-15\",\"amount\":\"$50\",\"purpose\":\"Office supplies including pens, notebooks, and printer paper\"}" 128 | } 129 | ] 130 | } 131 | } 132 | }' 133 | 134 | call_api "Submit reimbursement form" "$FORM_SUBMIT" "2" 135 | 136 | # Example 3: Incomplete form submission 137 | echo -e "${YELLOW}Example 3: Starting a new request with incomplete information${NC}" 138 | echo -e "This demonstrates how the agent handles incomplete information.\n" 139 | 140 | INCOMPLETE_REQUEST='{ 141 | "jsonrpc": "2.0", 142 | "method": "tasks/send", 143 | "id": 3, 144 | "params": { 145 | "id": "test-reimburse-3", 146 | "message": { 147 | "role": "user", 148 | "parts": [ 149 | { 150 | "type": "text", 151 | "text": "I need to get reimbursed for some expenses." 152 | } 153 | ] 154 | } 155 | } 156 | }' 157 | 158 | RESPONSE2=$(call_api "Incomplete reimbursement request" "$INCOMPLETE_REQUEST" "3") 159 | 160 | # Extract request_id from the second response 161 | REQUEST_ID2=$(extract_request_id "$RESPONSE2") 162 | if [ -z "$REQUEST_ID2" ]; then 163 | echo -e "${YELLOW}Couldn't extract request_id from response. Using fallback ID.${NC}" 164 | # Use a fallback ID since we can't extract it 165 | REQUEST_ID2="request_id_$(date +%s)" 166 | else 167 | echo -e "${GREEN}Extracted request_id: $REQUEST_ID2${NC}" 168 | fi 169 | 170 | # Wait for processing 171 | echo -e "${YELLOW}Waiting for the request to complete...${NC}" 172 | sleep 2 173 | 174 | # Example 4: Plain text form submission 175 | echo -e "${YELLOW}Example 4: Submitting a form in plain text format${NC}" 176 | echo -e "This demonstrates the agent's ability to parse form data from plain text.\n" 177 | 178 | PLAIN_TEXT_SUBMIT='{ 179 | "jsonrpc": "2.0", 180 | "method": "tasks/send", 181 | "id": 4, 182 | "params": { 183 | "id": "test-reimburse-4", 184 | "sessionId": "test-reimburse-3", 185 | "message": { 186 | "role": "user", 187 | "parts": [ 188 | { 189 | "type": "text", 190 | "text": "request_id: '"$REQUEST_ID2"'\ndate: 2023-12-01\namount: $75.50\npurpose: Team lunch meeting" 191 | } 192 | ] 193 | } 194 | } 195 | }' 196 | 197 | call_api "Plain text form submission" "$PLAIN_TEXT_SUBMIT" "4" 198 | 199 | echo -e "\n${GREEN}=====================================================================${NC}" 200 | echo -e "${GREEN} Demo Complete ${NC}" 201 | echo -e "${GREEN}=====================================================================${NC}" 202 | echo -e "${YELLOW}Usage Tips:${NC}" 203 | echo -e "1. Use the same sessionId for follow-up requests to maintain context" 204 | echo -e "2. The agent will generate a request_id which must be included in form submissions" 205 | echo -e "3. Form data can be submitted in either JSON or plain text format" 206 | echo -e "4. Run the Reimbursement Agent with: go run main.go\n" 207 | -------------------------------------------------------------------------------- /examples/simple/client/main.go: -------------------------------------------------------------------------------- 1 | // Tencent is pleased to support the open source community by making trpc-a2a-go available. 2 | // 3 | // Copyright (C) 2025 THL A29 Limited, a Tencent company. All rights reserved. 4 | // 5 | // trpc-a2a-go is licensed under the Apache License Version 2.0. 6 | 7 | // Package main implements a simple A2A client example. 8 | package main 9 | 10 | import ( 11 | "context" 12 | "flag" 13 | "fmt" 14 | "log" 15 | "time" 16 | 17 | "github.com/google/uuid" 18 | 19 | "trpc.group/trpc-go/trpc-a2a-go/client" 20 | "trpc.group/trpc-go/trpc-a2a-go/protocol" 21 | ) 22 | 23 | func main() { 24 | // Parse command-line flags. 25 | agentURL := flag.String("agent", "http://localhost:8080/", "Target A2A agent URL") 26 | timeout := flag.Duration("timeout", 30*time.Second, "Request timeout (e.g., 30s, 1m)") 27 | message := flag.String("message", "Hello, world!", "Message to send to the agent") 28 | flag.Parse() 29 | 30 | // Create A2A client. 31 | a2aClient, err := client.NewA2AClient(*agentURL, client.WithTimeout(*timeout)) 32 | if err != nil { 33 | log.Fatalf("Failed to create A2A client: %v", err) 34 | } 35 | 36 | // Display connection information. 37 | log.Printf("Connecting to agent: %s (Timeout: %v)", *agentURL, *timeout) 38 | 39 | // Create a new unique task ID. 40 | taskID := uuid.New().String() 41 | 42 | // Create a new session ID. 43 | sessionID := uuid.New().String() 44 | log.Printf("Session ID: %s", sessionID) 45 | 46 | // Create the message to send. 47 | userMessage := protocol.NewMessage( 48 | protocol.MessageRoleUser, 49 | []protocol.Part{protocol.NewTextPart(*message)}, 50 | ) 51 | 52 | // Create task parameters. 53 | params := protocol.SendTaskParams{ 54 | ID: taskID, 55 | SessionID: &sessionID, 56 | Message: userMessage, 57 | } 58 | 59 | log.Printf("Sending task %s with message: %s", taskID, *message) 60 | 61 | // Send task to the agent. 62 | ctx, cancel := context.WithTimeout(context.Background(), *timeout) 63 | defer cancel() 64 | 65 | task, err := a2aClient.SendTasks(ctx, params) 66 | if err != nil { 67 | log.Fatalf("Failed to send task: %v", err) 68 | } 69 | 70 | // Display the initial task response. 71 | log.Printf("Task %s initial state: %s", taskID, task.Status.State) 72 | 73 | // Wait for the task to complete if it's not already done. 74 | if task.Status.State != protocol.TaskStateCompleted && 75 | task.Status.State != protocol.TaskStateFailed && 76 | task.Status.State != protocol.TaskStateCanceled { 77 | 78 | log.Printf("Task %s is %s, fetching final state...", taskID, task.Status.State) 79 | 80 | // Get the task's final state. 81 | queryParams := protocol.TaskQueryParams{ 82 | ID: taskID, 83 | } 84 | 85 | // Give the server some time to process. 86 | time.Sleep(500 * time.Millisecond) 87 | 88 | task, err = a2aClient.GetTasks(ctx, queryParams) 89 | if err != nil { 90 | log.Fatalf("Failed to get task status: %v", err) 91 | } 92 | } 93 | 94 | // Display the final task state. 95 | log.Printf("Task %s final state: %s", taskID, task.Status.State) 96 | 97 | // Display the response message if available. 98 | if task.Status.Message != nil { 99 | fmt.Println("\nAgent response:") 100 | for _, part := range task.Status.Message.Parts { 101 | if textPart, ok := part.(protocol.TextPart); ok { 102 | fmt.Println(textPart.Text) 103 | } 104 | } 105 | } 106 | 107 | // Display any artifacts. 108 | if len(task.Artifacts) > 0 { 109 | fmt.Println("\nArtifacts:") 110 | for i, artifact := range task.Artifacts { 111 | // Display artifact name and description if available. 112 | if artifact.Name != nil { 113 | fmt.Printf("%d. %s", i+1, *artifact.Name) 114 | if artifact.Description != nil { 115 | fmt.Printf(" - %s", *artifact.Description) 116 | } 117 | fmt.Println() 118 | } else { 119 | fmt.Printf("%d. Artifact #%d\n", i+1, i+1) 120 | } 121 | 122 | // Display artifact content. 123 | for _, part := range artifact.Parts { 124 | if textPart, ok := part.(protocol.TextPart); ok { 125 | fmt.Printf(" %s\n", textPart.Text) 126 | } 127 | } 128 | } 129 | } 130 | } -------------------------------------------------------------------------------- /examples/simple/server/main.go: -------------------------------------------------------------------------------- 1 | // Tencent is pleased to support the open source community by making trpc-a2a-go available. 2 | // 3 | // Copyright (C) 2025 THL A29 Limited, a Tencent company. All rights reserved. 4 | // 5 | // trpc-a2a-go is licensed under the Apache License Version 2.0. 6 | 7 | // Package main implements a simple A2A server example. 8 | package main 9 | 10 | import ( 11 | "context" 12 | "flag" 13 | "fmt" 14 | "log" 15 | "os" 16 | "os/signal" 17 | "syscall" 18 | 19 | "trpc.group/trpc-go/trpc-a2a-go/protocol" 20 | "trpc.group/trpc-go/trpc-a2a-go/server" 21 | "trpc.group/trpc-go/trpc-a2a-go/taskmanager" 22 | ) 23 | 24 | // simpleTaskProcessor implements the taskmanager.TaskProcessor interface. 25 | type simpleTaskProcessor struct{} 26 | 27 | // Process implements the taskmanager.TaskProcessor interface. 28 | func (p *simpleTaskProcessor) Process( 29 | ctx context.Context, 30 | taskID string, 31 | message protocol.Message, 32 | handle taskmanager.TaskHandle, 33 | ) error { 34 | // Extract text from the incoming message. 35 | text := extractText(message) 36 | if text == "" { 37 | errMsg := "input message must contain text." 38 | log.Printf("Task %s failed: %s", taskID, errMsg) 39 | 40 | // Update status to Failed via handle. 41 | failedMessage := protocol.NewMessage( 42 | protocol.MessageRoleAgent, 43 | []protocol.Part{protocol.NewTextPart(errMsg)}, 44 | ) 45 | _ = handle.UpdateStatus(protocol.TaskStateFailed, &failedMessage) 46 | return fmt.Errorf(errMsg) 47 | } 48 | 49 | log.Printf("Processing task %s with input: %s", taskID, text) 50 | 51 | // Process the input text (in this simple example, we'll just reverse it). 52 | result := reverseString(text) 53 | 54 | // Create response message. 55 | responseMessage := protocol.NewMessage( 56 | protocol.MessageRoleAgent, 57 | []protocol.Part{protocol.NewTextPart(fmt.Sprintf("Processed result: %s", result))}, 58 | ) 59 | 60 | // Update task status to completed. 61 | if err := handle.UpdateStatus(protocol.TaskStateCompleted, &responseMessage); err != nil { 62 | return fmt.Errorf("failed to update task status: %w", err) 63 | } 64 | 65 | // Add the processed text as an artifact. 66 | artifact := protocol.Artifact{ 67 | Name: stringPtr("Reversed Text"), 68 | Description: stringPtr("The input text reversed"), 69 | Index: 0, 70 | Parts: []protocol.Part{protocol.NewTextPart(result)}, 71 | LastChunk: boolPtr(true), 72 | } 73 | 74 | if err := handle.AddArtifact(artifact); err != nil { 75 | log.Printf("Error adding artifact for task %s: %v", taskID, err) 76 | } 77 | 78 | return nil 79 | } 80 | 81 | // extractText extracts the text content from a message. 82 | func extractText(message protocol.Message) string { 83 | for _, part := range message.Parts { 84 | if textPart, ok := part.(protocol.TextPart); ok { 85 | return textPart.Text 86 | } 87 | } 88 | return "" 89 | } 90 | 91 | // reverseString reverses a string. 92 | func reverseString(s string) string { 93 | runes := []rune(s) 94 | for i, j := 0, len(runes)-1; i < j; i, j = i+1, j-1 { 95 | runes[i], runes[j] = runes[j], runes[i] 96 | } 97 | return string(runes) 98 | } 99 | 100 | // Helper function to create string pointers. 101 | func stringPtr(s string) *string { 102 | return &s 103 | } 104 | 105 | // Helper function to create bool pointers. 106 | func boolPtr(b bool) *bool { 107 | return &b 108 | } 109 | 110 | func main() { 111 | // Parse command-line flags. 112 | host := flag.String("host", "localhost", "Host to listen on") 113 | port := flag.Int("port", 8080, "Port to listen on") 114 | flag.Parse() 115 | 116 | // Create the agent card. 117 | agentCard := server.AgentCard{ 118 | Name: "Simple A2A Example Server", 119 | Description: stringPtr("A simple example A2A server that reverses text"), 120 | URL: fmt.Sprintf("http://%s:%d/", *host, *port), 121 | Version: "1.0.0", 122 | Provider: &server.AgentProvider{ 123 | Organization: "tRPC-A2A-Go Examples", 124 | }, 125 | Capabilities: server.AgentCapabilities{ 126 | Streaming: false, 127 | StateTransitionHistory: true, 128 | }, 129 | DefaultInputModes: []string{string(protocol.PartTypeText)}, 130 | DefaultOutputModes: []string{string(protocol.PartTypeText)}, 131 | Skills: []server.AgentSkill{ 132 | { 133 | ID: "text_reversal", 134 | Name: "Text Reversal", 135 | Description: stringPtr("Reverses the input text"), 136 | Tags: []string{"text", "processing"}, 137 | Examples: []string{"Hello, world!"}, 138 | InputModes: []string{string(protocol.PartTypeText)}, 139 | OutputModes: []string{string(protocol.PartTypeText)}, 140 | }, 141 | }, 142 | } 143 | 144 | // Create the task processor. 145 | processor := &simpleTaskProcessor{} 146 | 147 | // Create task manager and inject processor. 148 | taskManager, err := taskmanager.NewMemoryTaskManager(processor) 149 | if err != nil { 150 | log.Fatalf("Failed to create task manager: %v", err) 151 | } 152 | 153 | // Create the server. 154 | srv, err := server.NewA2AServer(agentCard, taskManager) 155 | if err != nil { 156 | log.Fatalf("Failed to create server: %v", err) 157 | } 158 | 159 | // Set up a channel to listen for termination signals. 160 | sigChan := make(chan os.Signal, 1) 161 | signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) 162 | 163 | // Start the server in a goroutine. 164 | go func() { 165 | serverAddr := fmt.Sprintf("%s:%d", *host, *port) 166 | log.Printf("Starting server on %s...", serverAddr) 167 | if err := srv.Start(serverAddr); err != nil { 168 | log.Fatalf("Server failed: %v", err) 169 | } 170 | }() 171 | 172 | // Wait for termination signal. 173 | sig := <-sigChan 174 | log.Printf("Received signal %v, shutting down...", sig) 175 | } 176 | -------------------------------------------------------------------------------- /examples/streaming/client/main.go: -------------------------------------------------------------------------------- 1 | // Tencent is pleased to support the open source community by making trpc-a2a-go available. 2 | // 3 | // Copyright (C) 2025 THL A29 Limited, a Tencent company. All rights reserved. 4 | // 5 | // trpc-a2a-go is licensed under the Apache License Version 2.0. 6 | 7 | // Package main implements a basic example of streaming a task from a client to a server. 8 | package main 9 | 10 | import ( 11 | "context" 12 | "encoding/json" 13 | "fmt" 14 | "io" 15 | "net/http" 16 | "time" 17 | 18 | "github.com/google/uuid" 19 | 20 | "trpc.group/trpc-go/trpc-a2a-go/client" 21 | "trpc.group/trpc-go/trpc-a2a-go/log" 22 | "trpc.group/trpc-go/trpc-a2a-go/protocol" 23 | "trpc.group/trpc-go/trpc-a2a-go/server" 24 | ) 25 | 26 | func main() { 27 | serverURL := "http://localhost:8080" 28 | 29 | // 1. Create a new client instance. 30 | c, err := client.NewA2AClient(serverURL) 31 | if err != nil { 32 | log.Fatalf("Error creating client: %v.", err) 33 | } 34 | 35 | // 2. Check for streaming capability by fetching the agent card 36 | streamingSupported, err := checkStreamingSupport(serverURL) 37 | if err != nil { 38 | log.Warnf("Could not determine streaming capability, assuming supported: %v.", err) 39 | streamingSupported = true // Assume supported if check fails 40 | } 41 | 42 | log.Infof("Server streaming capability: %t", streamingSupported) 43 | 44 | // 3. Define the task specification with a unique ID 45 | taskID := fmt.Sprintf("task-%d-%s", time.Now().UnixNano(), uuid.New().String()) 46 | message := protocol.Message{ 47 | Role: protocol.MessageRoleUser, 48 | Parts: []protocol.Part{ 49 | protocol.NewTextPart("Process this streaming data chunk by chunk."), 50 | }, 51 | } 52 | 53 | // Create context with timeout for the operation 54 | ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) 55 | defer cancel() 56 | 57 | if streamingSupported { 58 | // 4. Use streaming approach if supported 59 | log.Info("Server supports streaming, using StreamTask...") 60 | 61 | taskParams := protocol.SendTaskParams{ 62 | ID: taskID, 63 | Message: message, 64 | } 65 | 66 | streamChan, err := c.StreamTask(ctx, taskParams) 67 | if err != nil { 68 | log.Fatalf("Error starting stream task: %v.", err) 69 | } 70 | 71 | processStreamEvents(ctx, streamChan) 72 | } else { 73 | // 5. Fallback to non-streaming approach 74 | log.Info("Server does not support streaming, using SendTasks...") 75 | 76 | taskParams := protocol.SendTaskParams{ 77 | ID: taskID, 78 | Message: message, 79 | } 80 | 81 | task, err := c.SendTasks(ctx, taskParams) 82 | if err != nil { 83 | log.Fatalf("Error sending task: %v.", err) 84 | } 85 | 86 | log.Infof("Task completed with state: %s", task.Status.State) 87 | if task.Status.Message != nil { 88 | log.Infof("Final message: Role=%s", task.Status.Message.Role) 89 | for i, part := range task.Status.Message.Parts { 90 | if textPart, ok := part.(protocol.TextPart); ok { 91 | log.Infof(" Part %d text: %s", i, textPart.Text) 92 | } 93 | } 94 | } 95 | 96 | if len(task.Artifacts) > 0 { 97 | log.Infof("Task produced %d artifacts:", len(task.Artifacts)) 98 | for i, artifact := range task.Artifacts { 99 | log.Infof(" Artifact %d: %s", i, getArtifactName(artifact)) 100 | } 101 | } 102 | } 103 | } 104 | 105 | // checkStreamingSupport fetches the server's agent card to check if streaming is supported 106 | func checkStreamingSupport(serverURL string) (bool, error) { 107 | // According to the A2A protocol, agent cards are available at protocol.AgentCardPath 108 | agentCardURL := serverURL 109 | if agentCardURL[len(agentCardURL)-1] != '/' { 110 | agentCardURL += "/" 111 | } 112 | // Use the constant defined in the protocol package instead of hardcoding the path 113 | agentCardURL += protocol.AgentCardPath[1:] // Remove leading slash as we already have one 114 | 115 | // Create a request with a short timeout 116 | ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) 117 | defer cancel() 118 | 119 | req, err := http.NewRequestWithContext(ctx, http.MethodGet, agentCardURL, nil) 120 | if err != nil { 121 | return false, fmt.Errorf("error creating request: %w", err) 122 | } 123 | 124 | // Make the request 125 | resp, err := http.DefaultClient.Do(req) 126 | if err != nil { 127 | return false, fmt.Errorf("error fetching agent card: %w", err) 128 | } 129 | defer resp.Body.Close() 130 | 131 | // Check response status 132 | if resp.StatusCode != http.StatusOK { 133 | return false, fmt.Errorf("unexpected status code: %d", resp.StatusCode) 134 | } 135 | 136 | // Read and parse the agent card 137 | body, err := io.ReadAll(resp.Body) 138 | if err != nil { 139 | return false, fmt.Errorf("error reading response body: %w", err) 140 | } 141 | 142 | var agentCard server.AgentCard 143 | if err := json.Unmarshal(body, &agentCard); err != nil { 144 | return false, fmt.Errorf("error parsing agent card: %w", err) 145 | } 146 | 147 | return agentCard.Capabilities.Streaming, nil 148 | } 149 | 150 | // processStreamEvents handles events received from a streaming task 151 | func processStreamEvents(ctx context.Context, streamChan <-chan protocol.TaskEvent) { 152 | log.Info("Waiting for streaming updates...") 153 | 154 | for { 155 | select { 156 | case <-ctx.Done(): 157 | // Context timed out or was cancelled 158 | log.Infof("Streaming context done: %v", ctx.Err()) 159 | return 160 | case event, ok := <-streamChan: 161 | if !ok { 162 | // Channel closed by the client/server 163 | log.Info("Stream closed.") 164 | if ctx.Err() != nil { 165 | log.Infof("Context error after stream close: %v", ctx.Err()) 166 | } 167 | return 168 | } 169 | 170 | // Process the received event 171 | switch e := event.(type) { 172 | case protocol.TaskStatusUpdateEvent: 173 | log.Infof("Received Status Update - TaskID: %s, State: %s, Final: %t", e.ID, e.Status.State, e.Final) 174 | if e.Status.Message != nil { 175 | log.Infof(" Status Message: Role=%s, Parts=%+v", e.Status.Message.Role, e.Status.Message.Parts) 176 | } 177 | 178 | // Exit when we receive a final status update (indicating a terminal state) 179 | // Per A2A spec, this should be the definitive way to know the task is complete 180 | if e.IsFinal() { 181 | if e.Status.State == protocol.TaskStateCompleted { 182 | log.Info("Task completed successfully.") 183 | } else if e.Status.State == protocol.TaskStateFailed { 184 | log.Info("Task failed.") 185 | } else if e.Status.State == protocol.TaskStateCanceled { 186 | log.Info("Task was canceled.") 187 | } 188 | log.Info("Received final status update, exiting.") 189 | return 190 | } 191 | case protocol.TaskArtifactUpdateEvent: 192 | log.Infof("Received Artifact Update - TaskID: %s, Index: %d, Append: %v, LastChunk: %v", 193 | e.ID, e.Artifact.Index, e.Artifact.Append, e.Artifact.LastChunk) 194 | log.Infof(" Artifact Parts: %+v", e.Artifact.Parts) 195 | 196 | // For artifact updates, we note it's the final artifact, 197 | // but we don't exit yet - per A2A spec, we should wait for the final status update 198 | if e.IsFinal() { 199 | log.Info("Received final artifact update, waiting for final status.") 200 | } 201 | default: 202 | log.Infof("Received unknown event type: %T %v", event, event) 203 | } 204 | } 205 | } 206 | } 207 | 208 | // getArtifactName returns the name of an artifact or a default if name is nil 209 | func getArtifactName(artifact protocol.Artifact) string { 210 | if artifact.Name != nil { 211 | return *artifact.Name 212 | } 213 | return fmt.Sprintf("Unnamed artifact (index: %d)", artifact.Index) 214 | } 215 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module trpc.group/trpc-go/trpc-a2a-go 2 | 3 | go 1.23.0 4 | 5 | toolchain go1.23.7 6 | 7 | require ( 8 | github.com/golang-jwt/jwt/v5 v5.2.2 9 | github.com/lestrrat-go/jwx/v2 v2.1.4 10 | github.com/stretchr/testify v1.10.0 11 | go.uber.org/zap v1.27.0 12 | golang.org/x/oauth2 v0.29.0 13 | ) 14 | 15 | require ( 16 | github.com/davecgh/go-spew v1.1.1 // indirect 17 | github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect 18 | github.com/goccy/go-json v0.10.3 // indirect 19 | github.com/lestrrat-go/blackmagic v1.0.2 // indirect 20 | github.com/lestrrat-go/httpcc v1.0.1 // indirect 21 | github.com/lestrrat-go/httprc v1.0.6 // indirect 22 | github.com/lestrrat-go/iter v1.0.2 // indirect 23 | github.com/lestrrat-go/option v1.0.1 // indirect 24 | github.com/pmezard/go-difflib v1.0.0 // indirect 25 | github.com/segmentio/asm v1.2.0 // indirect 26 | go.uber.org/multierr v1.10.0 // indirect 27 | golang.org/x/crypto v0.35.0 // indirect 28 | golang.org/x/sys v0.30.0 // indirect 29 | gopkg.in/yaml.v3 v3.0.1 // indirect 30 | ) 31 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc= 5 | github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40= 6 | github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA= 7 | github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= 8 | github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= 9 | github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= 10 | github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= 11 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 12 | github.com/lestrrat-go/blackmagic v1.0.2 h1:Cg2gVSc9h7sz9NOByczrbUvLopQmXrfFx//N+AkAr5k= 13 | github.com/lestrrat-go/blackmagic v1.0.2/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU= 14 | github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE= 15 | github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E= 16 | github.com/lestrrat-go/httprc v1.0.6 h1:qgmgIRhpvBqexMJjA/PmwSvhNk679oqD1RbovdCGW8k= 17 | github.com/lestrrat-go/httprc v1.0.6/go.mod h1:mwwz3JMTPBjHUkkDv/IGJ39aALInZLrhBp0X7KGUZlo= 18 | github.com/lestrrat-go/iter v1.0.2 h1:gMXo1q4c2pHmC3dn8LzRhJfP1ceCbgSiT9lUydIzltI= 19 | github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4= 20 | github.com/lestrrat-go/jwx/v2 v2.1.4 h1:uBCMmJX8oRZStmKuMMOFb0Yh9xmEMgNJLgjuKKt4/qc= 21 | github.com/lestrrat-go/jwx/v2 v2.1.4/go.mod h1:nWRbDFR1ALG2Z6GJbBXzfQaYyvn751KuuyySN2yR6is= 22 | github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU= 23 | github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= 24 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 25 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 26 | github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys= 27 | github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= 28 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 29 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 30 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 31 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 32 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 33 | go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= 34 | go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= 35 | go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= 36 | go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= 37 | go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= 38 | go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= 39 | golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs= 40 | golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ= 41 | golang.org/x/oauth2 v0.29.0 h1:WdYw2tdTK1S8olAzWHdgeqfy+Mtm9XNhv/xJsY65d98= 42 | golang.org/x/oauth2 v0.29.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= 43 | golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= 44 | golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 45 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 46 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 47 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 48 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 49 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 50 | -------------------------------------------------------------------------------- /internal/jsonrpc/request.go: -------------------------------------------------------------------------------- 1 | // Tencent is pleased to support the open source community by making trpc-a2a-go available. 2 | // 3 | // Copyright (C) 2025 THL A29 Limited, a Tencent company. All rights reserved. 4 | // 5 | // trpc-a2a-go is licensed under the Apache License Version 2.0. 6 | 7 | package jsonrpc 8 | 9 | import "encoding/json" 10 | 11 | // Request represents a JSON-RPC request object. 12 | type Request struct { 13 | Message 14 | // Method is a String containing the name of the method to be invoked. 15 | Method string `json:"method"` 16 | // Params is a Structured value that holds the parameter values to be used 17 | // during the invocation of the method. This member MAY be omitted. 18 | // It's stored as raw JSON to be decoded by the method handler. 19 | Params json.RawMessage `json:"params,omitempty"` 20 | } 21 | 22 | // NewRequest creates a new JSON-RPC request with the given method and ID. 23 | func NewRequest(method string, id interface{}) *Request { 24 | return &Request{ 25 | Message: Message{ 26 | JSONRPC: Version, 27 | ID: id, 28 | }, 29 | Method: method, 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /internal/jsonrpc/request_test.go: -------------------------------------------------------------------------------- 1 | // Tencent is pleased to support the open source community by making trpc-a2a-go available. 2 | // 3 | // Copyright (C) 2025 THL A29 Limited, a Tencent company. All rights reserved. 4 | // 5 | // trpc-a2a-go is licensed under the Apache License Version 2.0. 6 | 7 | package jsonrpc 8 | 9 | import ( 10 | "encoding/json" 11 | "testing" 12 | 13 | "github.com/stretchr/testify/assert" 14 | "github.com/stretchr/testify/require" 15 | ) 16 | 17 | func TestCreateNewRequest(t *testing.T) { 18 | // Test creating a new request 19 | req := NewRequest("test-method", "test-id") 20 | 21 | assert.Equal(t, Version, req.JSONRPC) 22 | assert.Equal(t, "test-id", req.ID) 23 | assert.Equal(t, "test-method", req.Method) 24 | assert.Nil(t, req.Params) 25 | } 26 | 27 | func TestRequestMarshalJSON(t *testing.T) { 28 | // Test with all fields populated 29 | params := json.RawMessage(`{"key":"value"}`) 30 | req := &Request{ 31 | Message: Message{ 32 | JSONRPC: Version, 33 | ID: "test-id", 34 | }, 35 | Method: "test-method", 36 | Params: params, 37 | } 38 | 39 | data, err := json.Marshal(req) 40 | require.NoError(t, err) 41 | 42 | var unmarshaled map[string]interface{} 43 | err = json.Unmarshal(data, &unmarshaled) 44 | require.NoError(t, err) 45 | 46 | assert.Equal(t, Version, unmarshaled["jsonrpc"]) 47 | assert.Equal(t, "test-id", unmarshaled["id"]) 48 | assert.Equal(t, "test-method", unmarshaled["method"]) 49 | 50 | paramsMap, ok := unmarshaled["params"].(map[string]interface{}) 51 | require.True(t, ok) 52 | assert.Equal(t, "value", paramsMap["key"]) 53 | 54 | // Test with empty params 55 | req = &Request{ 56 | Message: Message{ 57 | JSONRPC: Version, 58 | ID: "test-id", 59 | }, 60 | Method: "test-method", 61 | } 62 | 63 | data, err = json.Marshal(req) 64 | require.NoError(t, err) 65 | 66 | unmarshaled = make(map[string]interface{}) 67 | err = json.Unmarshal(data, &unmarshaled) 68 | require.NoError(t, err) 69 | 70 | assert.Equal(t, Version, unmarshaled["jsonrpc"]) 71 | assert.Equal(t, "test-id", unmarshaled["id"]) 72 | assert.Equal(t, "test-method", unmarshaled["method"]) 73 | 74 | // Check that params field is not present in the JSON output 75 | _, exists := unmarshaled["params"] 76 | assert.False(t, exists, "params field should not be present when empty") 77 | } 78 | 79 | func TestRequestUnmarshalJSON(t *testing.T) { 80 | // Test valid JSON-RPC request 81 | jsonData := `{ 82 | "jsonrpc": "2.0", 83 | "id": "request-id", 84 | "method": "test.method", 85 | "params": {"param1": "value1", "param2": 42} 86 | }` 87 | 88 | var req Request 89 | err := json.Unmarshal([]byte(jsonData), &req) 90 | require.NoError(t, err) 91 | 92 | assert.Equal(t, Version, req.JSONRPC) 93 | assert.Equal(t, "request-id", req.ID) 94 | assert.Equal(t, "test.method", req.Method) 95 | 96 | // Verify params 97 | var params map[string]interface{} 98 | err = json.Unmarshal(req.Params, ¶ms) 99 | require.NoError(t, err) 100 | assert.Equal(t, "value1", params["param1"]) 101 | assert.Equal(t, float64(42), params["param2"]) 102 | 103 | // Test without params 104 | jsonData = `{ 105 | "jsonrpc": "2.0", 106 | "id": "request-id", 107 | "method": "test.method" 108 | }` 109 | 110 | req = Request{} 111 | err = json.Unmarshal([]byte(jsonData), &req) 112 | require.NoError(t, err) 113 | 114 | assert.Equal(t, Version, req.JSONRPC) 115 | assert.Equal(t, "request-id", req.ID) 116 | assert.Equal(t, "test.method", req.Method) 117 | assert.Len(t, req.Params, 0, "params should be empty") 118 | 119 | // Test with missing jsonrpc version 120 | jsonData = `{ 121 | "id": "request-id", 122 | "method": "test.method" 123 | }` 124 | 125 | req = Request{} 126 | err = json.Unmarshal([]byte(jsonData), &req) 127 | require.NoError(t, err) // Will not fail because jsonrpc is not validated in unmarshal 128 | 129 | // Test with missing required method field 130 | jsonData = `{ 131 | "jsonrpc": "2.0", 132 | "id": "request-id" 133 | }` 134 | 135 | req = Request{} 136 | err = json.Unmarshal([]byte(jsonData), &req) 137 | require.NoError(t, err) // Will not fail because method is not validated in unmarshal 138 | 139 | // Test with invalid JSON 140 | jsonData = `{ 141 | "jsonrpc": "2.0", 142 | "id": "request-id", 143 | "method": "test.method", 144 | "params": {"invalid": 145 | }` 146 | 147 | req = Request{} 148 | err = json.Unmarshal([]byte(jsonData), &req) 149 | assert.Error(t, err) 150 | } 151 | -------------------------------------------------------------------------------- /internal/jsonrpc/response.go: -------------------------------------------------------------------------------- 1 | // Tencent is pleased to support the open source community by making trpc-a2a-go available. 2 | // 3 | // Copyright (C) 2025 THL A29 Limited, a Tencent company. All rights reserved. 4 | // 5 | // trpc-a2a-go is licensed under the Apache License Version 2.0. 6 | 7 | package jsonrpc 8 | 9 | // Response represents a JSON-RPC response object. 10 | // Either Result or Error MUST be included, but not both. 11 | type Response struct { 12 | Message 13 | // Result is REQUIRED on success. 14 | // This member MUST NOT exist if there was an error invoking the method. 15 | // The value of this member is determined by the method invoked on the Server. 16 | // It's stored as an interface{} and often requires type assertion or 17 | // further unmarshalling based on the expected method result. 18 | Result interface{} `json:"result,omitempty"` 19 | // Error is REQUIRED on error. 20 | // This member MUST NOT exist if there was no error triggered during invocation. 21 | // The value for this member MUST be an Object as defined in section 5.1. 22 | Error *Error `json:"error,omitempty"` 23 | } 24 | 25 | // NewResponse creates a new JSON-RPC response with a result. 26 | func NewResponse(id interface{}, result interface{}) *Response { 27 | return &Response{ 28 | Message: Message{JSONRPC: Version, ID: id}, 29 | Result: result, 30 | } 31 | } 32 | 33 | // NewErrorResponse creates a new JSON-RPC response with an error. 34 | func NewErrorResponse(id interface{}, err *Error) *Response { 35 | return &Response{ 36 | Message: Message{JSONRPC: Version, ID: id}, 37 | Error: err, 38 | } 39 | } 40 | 41 | // NewNotificationResponse creates a JSON-RPC notification response with an optional ID. 42 | // When ID is nil, it creates a proper notification (no ID field). 43 | // When ID is provided, it creates a response that includes the ID (used for SSE events). 44 | // This is useful for event streams like SSE where messages may need to be correlated 45 | // with the original request. 46 | func NewNotificationResponse(id interface{}, result interface{}) *Response { 47 | return &Response{ 48 | Message: Message{JSONRPC: Version, ID: id}, 49 | Result: result, 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /internal/jsonrpc/types.go: -------------------------------------------------------------------------------- 1 | // Tencent is pleased to support the open source community by making trpc-a2a-go available. 2 | // 3 | // Copyright (C) 2025 THL A29 Limited, a Tencent company. All rights reserved. 4 | // 5 | // trpc-a2a-go is licensed under the Apache License Version 2.0. 6 | 7 | // Package jsonrpc defines types and helpers for JSON-RPC 2.0 communication, 8 | // adhering to the specification at https://www.jsonrpc.org/specification. 9 | package jsonrpc 10 | 11 | import ( 12 | "encoding/json" 13 | "fmt" 14 | ) 15 | 16 | // Version is the JSON-RPC version. 17 | const Version = "2.0" 18 | 19 | // Standard JSON-RPC 2.0 error codes. 20 | const ( 21 | // CodeParseError indicates invalid JSON was received by the server. 22 | // An error occurred on the server while parsing the JSON text. 23 | CodeParseError = -32700 24 | // CodeInvalidRequest indicates the JSON sent is not a valid Request object. 25 | CodeInvalidRequest = -32600 26 | // CodeMethodNotFound indicates the method does not exist / is not available. 27 | CodeMethodNotFound = -32601 28 | // CodeInvalidParams indicates invalid method parameter(s). 29 | CodeInvalidParams = -32602 30 | // CodeInternalError indicates an internal JSON-RPC error. 31 | CodeInternalError = -32603 32 | // -32000 to -32099 are reserved for implementation-defined server-errors. 33 | ) 34 | 35 | // Message is the base structure embedding common fields for JSON-RPC 36 | // requests and responses. 37 | type Message struct { 38 | // JSONRPC specifies the version of the JSON-RPC protocol. MUST be "2.0". 39 | JSONRPC string `json:"jsonrpc"` 40 | // ID is an identifier established by the Client that MUST contain a String, 41 | // Number, or NULL value if included. If it is not included it is assumed 42 | // to be a notification. The value SHOULD normally not be Null and Numbers 43 | // SHOULD NOT contain fractional parts. 44 | ID interface{} `json:"id,omitempty"` 45 | } 46 | 47 | // RawResponse is a JSON-RPC response that includes the raw result as a 48 | // json.RawMessage. This is useful for APIs that return arbitrary JSON data. 49 | type RawResponse struct { 50 | Response // Embed base fields (id, jsonrpc, error). 51 | Result json.RawMessage `json:"result"` // Get result as raw JSON first. 52 | } 53 | 54 | // Error represents a JSON-RPC error object, included in responses when 55 | // an error occurs. 56 | type Error struct { 57 | // Code is a Number that indicates the error type that occurred. 58 | // This MUST be an integer. 59 | Code int `json:"code"` 60 | // Message is a String providing a short description of the error. 61 | // The message SHOULD be limited to a concise single sentence. 62 | Message string `json:"message"` 63 | // Data is a Primitive or Structured value that contains additional 64 | // information about the error. This may be omitted. 65 | // The value of this member is defined by the Server (e.g. detailed error 66 | // information, nested errors etc.). 67 | Data interface{} `json:"data,omitempty"` 68 | } 69 | 70 | // Error implements the standard Go error interface for JSONRPCError, providing 71 | // a basic string representation of the error. 72 | func (e *Error) Error() string { 73 | if e == nil { 74 | return "" 75 | } 76 | return fmt.Sprintf("jsonrpc error %d: %s", e.Code, e.Message) 77 | } 78 | 79 | // --- Standard Error Constructors --- 80 | 81 | // ErrParseError creates a standard Parse Error (-32700) JSONRPCError. 82 | // Use this when the server fails to parse the JSON request. 83 | func ErrParseError(data interface{}) *Error { 84 | return &Error{Code: CodeParseError, Message: "Parse error", Data: data} 85 | } 86 | 87 | // ErrInvalidRequest creates a standard Invalid Request error (-32600) JSONRPCError. 88 | // Use this when the JSON is valid, but the request object is not a valid 89 | // JSON-RPC Request (e.g., missing "jsonrpc" or "method"). 90 | func ErrInvalidRequest(data interface{}) *Error { 91 | return &Error{Code: CodeInvalidRequest, Message: "Invalid Request", Data: data} 92 | } 93 | 94 | // ErrMethodNotFound creates a standard Method Not Found error (-32601) JSONRPCError. 95 | // Use this when the requested method does not exist on the server. 96 | func ErrMethodNotFound(data interface{}) *Error { 97 | return &Error{Code: CodeMethodNotFound, Message: "Method not found", Data: data} 98 | } 99 | 100 | // ErrInvalidParams creates a standard Invalid Params error (-32602) JSONRPCError. 101 | // Use this when the method parameters are invalid (e.g., wrong type, missing fields). 102 | func ErrInvalidParams(data interface{}) *Error { 103 | return &Error{Code: CodeInvalidParams, Message: "Invalid params", Data: data} 104 | } 105 | 106 | // ErrInternalError creates a standard Internal Error (-32603) JSONRPCError. 107 | // Use this for generic internal server errors not covered by other codes. 108 | func ErrInternalError(data interface{}) *Error { 109 | return &Error{Code: CodeInternalError, Message: "Internal error", Data: data} 110 | } 111 | -------------------------------------------------------------------------------- /internal/sse/sse.go: -------------------------------------------------------------------------------- 1 | // Tencent is pleased to support the open source community by making trpc-a2a-go available. 2 | // 3 | // Copyright (C) 2025 THL A29 Limited, a Tencent company. All rights reserved. 4 | // 5 | // trpc-a2a-go is licensed under the Apache License Version 2.0. 6 | 7 | // Package sse provides a reader for Server-Sent Events (SSE). 8 | package sse 9 | 10 | import ( 11 | "bufio" 12 | "bytes" 13 | "encoding/json" 14 | "fmt" 15 | "io" 16 | 17 | "trpc.group/trpc-go/trpc-a2a-go/internal/jsonrpc" 18 | "trpc.group/trpc-go/trpc-a2a-go/log" 19 | ) 20 | 21 | // CloseEventData represents the data payload for a close event. 22 | // Used when formatting SSE messages indicating stream closure. 23 | type CloseEventData struct { 24 | TaskID string `json:"taskId"` 25 | Reason string `json:"reason"` 26 | } 27 | 28 | // EventReader helps parse text/event-stream formatted data. 29 | type EventReader struct { 30 | scanner *bufio.Scanner 31 | } 32 | 33 | // NewEventReader creates a new reader for SSE events. 34 | // Exported function. 35 | func NewEventReader(r io.Reader) *EventReader { 36 | scanner := bufio.NewScanner(r) 37 | return &EventReader{scanner: scanner} 38 | } 39 | 40 | // ReadEvent reads the next complete event from the stream. 41 | // It returns the event data, event type, and any error (including io.EOF). 42 | // Exported method. 43 | func (r *EventReader) ReadEvent() (data []byte, eventType string, err error) { 44 | dataBuffer := bytes.Buffer{} 45 | eventType = "message" // Default event type per SSE spec. 46 | for r.scanner.Scan() { 47 | line := r.scanner.Bytes() 48 | if len(line) == 0 { 49 | // Empty line signifies end of event. 50 | if dataBuffer.Len() > 0 { 51 | // We have data, return the completed event. 52 | // Remove the last newline added by the loop. 53 | d := dataBuffer.Bytes() 54 | if len(d) > 0 && d[len(d)-1] == '\n' { 55 | d = d[:len(d)-1] 56 | } 57 | return d, eventType, nil 58 | } 59 | // Double newline without data is just a keep-alive tick, ignore. 60 | continue 61 | } 62 | // Process field lines (e.g., "event: ", "data: ", "id: ", "retry: "). 63 | if bytes.HasPrefix(line, []byte("event:")) { 64 | eventType = string(bytes.TrimSpace(line[len("event:"):])) 65 | } else if bytes.HasPrefix(line, []byte("data:")) { 66 | // Append data field, preserving newlines within the data. 67 | dataChunk := line[len("data:"):] 68 | if len(dataChunk) > 0 && dataChunk[0] == ' ' { 69 | dataChunk = dataChunk[1:] // Trim leading space if present. 70 | } 71 | dataBuffer.Write(dataChunk) 72 | dataBuffer.WriteByte('\n') // Add newline between data chunks. 73 | } else if bytes.HasPrefix(line, []byte("id:")) { 74 | // Store or process last event ID (optional, ignored here). 75 | } else if bytes.HasPrefix(line, []byte("retry:")) { 76 | // Store or process retry timeout (optional, ignored here). 77 | } else if bytes.HasPrefix(line, []byte(":")) { 78 | // Comment line, ignore. 79 | } else { 80 | // Lines without a field prefix might be invalid per spec, 81 | // but some implementations might just treat them as data. 82 | // For robustness, let's treat it as data. 83 | log.Warnf("SSE line without recognized prefix: %s", string(line)) 84 | dataBuffer.Write(line) 85 | dataBuffer.WriteByte('\n') 86 | } 87 | } 88 | // Scanner finished, check for errors. 89 | if err := r.scanner.Err(); err != nil { 90 | return nil, "", err 91 | } 92 | // Check if there was remaining data when EOF was hit without a final newline. 93 | if dataBuffer.Len() > 0 { 94 | d := dataBuffer.Bytes() 95 | if len(d) > 0 && d[len(d)-1] == '\n' { 96 | d = d[:len(d)-1] 97 | } 98 | return d, eventType, io.EOF // Return data with EOF. 99 | } 100 | return nil, "", io.EOF // Normal EOF. 101 | } 102 | 103 | // FormatEvent marshals the given data to JSON and writes it to the writer 104 | // in the standard SSE format (event: type\\ndata: json\\n\\n). 105 | // It handles potential JSON marshaling errors. 106 | // Exported function. 107 | func FormatEvent(w io.Writer, eventType string, data interface{}) error { 108 | jsonData, err := json.Marshal(data) 109 | if err != nil { 110 | return fmt.Errorf("failed to marshal SSE event data: %w", err) 111 | } 112 | // Format according to text/event-stream specification. 113 | // event: 114 | // data: 115 | // 116 | if _, err := fmt.Fprintf(w, "event: %s\ndata: %s\n\n", eventType, string(jsonData)); err != nil { 117 | return fmt.Errorf("failed to write SSE event: %w", err) 118 | } 119 | return nil 120 | } 121 | 122 | // FormatJSONRPCEvent marshals the given data as a JSON-RPC response and writes 123 | // it to the writer in SSE format. The event type is used as the SSE event type. 124 | // The id parameter allows correlation with the original request. 125 | // It handles potential JSON marshaling errors. 126 | // Exported function. 127 | func FormatJSONRPCEvent(w io.Writer, eventType string, id interface{}, data interface{}) error { 128 | // Create a JSON-RPC response with the data as the result 129 | response := jsonrpc.NewNotificationResponse(id, data) 130 | // Marshal the entire JSON-RPC envelope 131 | jsonData, err := json.Marshal(response) 132 | if err != nil { 133 | return fmt.Errorf("failed to marshal JSON-RPC SSE event data: %w", err) 134 | } 135 | // Format according to text/event-stream specification 136 | // event: 137 | // data: 138 | // 139 | if _, err := fmt.Fprintf(w, "event: %s\ndata: %s\n\n", eventType, string(jsonData)); err != nil { 140 | return fmt.Errorf("failed to write JSON-RPC SSE event: %w", err) 141 | } 142 | return nil 143 | } 144 | -------------------------------------------------------------------------------- /internal/sse/sse_test.go: -------------------------------------------------------------------------------- 1 | package sse 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "encoding/json" 7 | "io" 8 | "strings" 9 | "testing" 10 | 11 | "github.com/stretchr/testify/assert" 12 | "trpc.group/trpc-go/trpc-a2a-go/internal/jsonrpc" 13 | ) 14 | 15 | func TestNewEventReader(t *testing.T) { 16 | r := strings.NewReader("test data") 17 | er := NewEventReader(r) 18 | if er == nil { 19 | t.Fatal("Expected non-nil EventReader.") 20 | } 21 | if er.scanner == nil { 22 | t.Fatal("Expected non-nil scanner in EventReader.") 23 | } 24 | } 25 | 26 | func TestReadEvent(t *testing.T) { 27 | tests := []struct { 28 | name string 29 | input string 30 | expectedData string 31 | expectedType string 32 | expectedError error 33 | expectNoEvent bool 34 | }{ 35 | { 36 | name: "simple event", 37 | input: "data: test data\n\n", 38 | expectedData: "test data", 39 | expectedType: "message", 40 | expectedError: nil, 41 | }, 42 | { 43 | name: "event with custom type", 44 | input: "event: custom\ndata: test data\n\n", 45 | expectedData: "test data", 46 | expectedType: "custom", 47 | expectedError: nil, 48 | }, 49 | { 50 | name: "event with multiple data lines", 51 | input: "data: line1\ndata: line2\n\n", 52 | expectedData: "line1\nline2", 53 | expectedType: "message", 54 | expectedError: nil, 55 | }, 56 | { 57 | name: "event with id and comments", 58 | input: "id: 123\n:comment\ndata: test data\n\n", 59 | expectedData: "test data", 60 | expectedType: "message", 61 | expectedError: nil, 62 | }, 63 | { 64 | name: "empty data", 65 | input: "data:\n\n", 66 | expectedData: "", 67 | expectedType: "message", 68 | expectedError: nil, 69 | }, 70 | { 71 | name: "keep-alive tick", 72 | input: "\n\n", 73 | expectNoEvent: true, 74 | }, 75 | { 76 | name: "data with EOF without final newline", 77 | input: "data: test data", 78 | expectedData: "test data", 79 | expectedType: "message", 80 | expectedError: io.EOF, 81 | }, 82 | { 83 | name: "empty input", 84 | input: "", 85 | expectedError: io.EOF, 86 | expectNoEvent: true, 87 | }, 88 | { 89 | name: "unrecognized line prefix", 90 | input: "unknown: value\ndata: test\n\n", 91 | expectedData: "unknown: value\ntest", 92 | expectedType: "message", 93 | expectedError: nil, 94 | }, 95 | } 96 | 97 | for _, tt := range tests { 98 | t.Run(tt.name, func(t *testing.T) { 99 | r := strings.NewReader(tt.input) 100 | er := NewEventReader(r) 101 | 102 | data, eventType, err := er.ReadEvent() 103 | 104 | if tt.expectNoEvent { 105 | if err != io.EOF { 106 | t.Errorf("Expected EOF for keep-alive tick, got %v.", err) 107 | } 108 | return 109 | } 110 | 111 | if (err == nil && tt.expectedError != nil) || (err != nil && tt.expectedError == nil) || 112 | (err != nil && tt.expectedError != nil && err.Error() != tt.expectedError.Error()) { 113 | t.Errorf("Expected error %v, got %v.", tt.expectedError, err) 114 | } 115 | 116 | if string(data) != tt.expectedData { 117 | t.Errorf("Expected data %q, got %q.", tt.expectedData, string(data)) 118 | } 119 | 120 | if eventType != tt.expectedType { 121 | t.Errorf("Expected event type %q, got %q.", tt.expectedType, eventType) 122 | } 123 | }) 124 | } 125 | } 126 | 127 | func TestFormatEvent(t *testing.T) { 128 | tests := []struct { 129 | name string 130 | eventType string 131 | data interface{} 132 | expected string 133 | shouldFail bool 134 | }{ 135 | { 136 | name: "simple string data", 137 | eventType: "message", 138 | data: "test data", 139 | expected: "event: message\ndata: \"test data\"\n\n", 140 | }, 141 | { 142 | name: "struct data", 143 | eventType: "close", 144 | data: CloseEventData{TaskID: "123", Reason: "completed"}, 145 | expected: "event: close\ndata: {\"taskId\":\"123\",\"reason\":\"completed\"}\n\n", 146 | }, 147 | { 148 | name: "marshal error", 149 | eventType: "error", 150 | data: make(chan int), // Cannot be marshaled to JSON 151 | shouldFail: true, 152 | }, 153 | } 154 | 155 | for _, tt := range tests { 156 | t.Run(tt.name, func(t *testing.T) { 157 | var buf bytes.Buffer 158 | err := FormatEvent(&buf, tt.eventType, tt.data) 159 | 160 | if tt.shouldFail { 161 | if err == nil { 162 | t.Error("Expected error, got nil.") 163 | } 164 | return 165 | } 166 | 167 | if err != nil { 168 | t.Errorf("Unexpected error: %v.", err) 169 | return 170 | } 171 | 172 | if buf.String() != tt.expected { 173 | t.Errorf("Expected output %q, got %q.", tt.expected, buf.String()) 174 | } 175 | }) 176 | } 177 | } 178 | 179 | func TestReadEventSequence(t *testing.T) { 180 | // Test reading multiple events in sequence 181 | input := "event: first\ndata: event1\n\nevent: second\ndata: event2\n\n" 182 | r := strings.NewReader(input) 183 | er := NewEventReader(r) 184 | 185 | // Read first event 186 | data1, type1, err1 := er.ReadEvent() 187 | if err1 != nil { 188 | t.Fatalf("Unexpected error reading first event: %v.", err1) 189 | } 190 | if string(data1) != "event1" { 191 | t.Errorf("Expected data %q, got %q.", "event1", string(data1)) 192 | } 193 | if type1 != "first" { 194 | t.Errorf("Expected event type %q, got %q.", "first", type1) 195 | } 196 | 197 | // Read second event 198 | data2, type2, err2 := er.ReadEvent() 199 | if err2 != nil { 200 | t.Fatalf("Unexpected error reading second event: %v.", err2) 201 | } 202 | if string(data2) != "event2" { 203 | t.Errorf("Expected data %q, got %q.", "event2", string(data2)) 204 | } 205 | if type2 != "second" { 206 | t.Errorf("Expected event type %q, got %q.", "second", type2) 207 | } 208 | 209 | // Should be at EOF now 210 | _, _, err3 := er.ReadEvent() 211 | if err3 != io.EOF { 212 | t.Errorf("Expected EOF, got %v.", err3) 213 | } 214 | } 215 | 216 | func TestCloseEventDataMarshaling(t *testing.T) { 217 | closeData := CloseEventData{ 218 | TaskID: "task123", 219 | Reason: "test completed", 220 | } 221 | 222 | jsonBytes, err := json.Marshal(closeData) 223 | if err != nil { 224 | t.Fatalf("Failed to marshal CloseEventData: %v.", err) 225 | } 226 | 227 | var unmarshaled CloseEventData 228 | if err := json.Unmarshal(jsonBytes, &unmarshaled); err != nil { 229 | t.Fatalf("Failed to unmarshal CloseEventData: %v.", err) 230 | } 231 | 232 | if unmarshaled.TaskID != closeData.TaskID { 233 | t.Errorf("Expected TaskID %q, got %q.", closeData.TaskID, unmarshaled.TaskID) 234 | } 235 | 236 | if unmarshaled.Reason != closeData.Reason { 237 | t.Errorf("Expected Reason %q, got %q.", closeData.Reason, unmarshaled.Reason) 238 | } 239 | } 240 | 241 | func TestFormatJSONRPCEvent(t *testing.T) { 242 | // Test data 243 | eventType := "test_event" 244 | eventID := "test-request-123" 245 | eventData := map[string]string{ 246 | "key1": "value1", 247 | "key2": "value2", 248 | } 249 | 250 | // Buffer to capture the output 251 | var buf bytes.Buffer 252 | 253 | // Format the event 254 | err := FormatJSONRPCEvent(&buf, eventType, eventID, eventData) 255 | assert.NoError(t, err, "FormatJSONRPCEvent should not return an error") 256 | 257 | // Get the formatted output 258 | output := buf.String() 259 | 260 | // Verify the SSE format structure 261 | assert.Contains(t, output, "event: test_event", "Output should contain the event type") 262 | assert.Contains(t, output, "data: {", "Output should contain JSON data") 263 | assert.Contains(t, output, "\"jsonrpc\":\"2.0\"", "Output should contain JSON-RPC version") 264 | assert.Contains(t, output, "\"id\":\"test-request-123\"", "Output should contain the request ID") 265 | 266 | // Verify the content can be parsed back 267 | scanner := bufio.NewScanner(strings.NewReader(output)) 268 | var dataLine string 269 | for scanner.Scan() { 270 | line := scanner.Text() 271 | if strings.HasPrefix(line, "data: ") { 272 | dataLine = strings.TrimPrefix(line, "data: ") 273 | break 274 | } 275 | } 276 | 277 | // Parse the JSON-RPC response 278 | var response jsonrpc.Response 279 | err = json.Unmarshal([]byte(dataLine), &response) 280 | assert.NoError(t, err, "Should be able to unmarshal the JSON-RPC response") 281 | 282 | // Check response structure 283 | assert.Equal(t, "2.0", response.JSONRPC, "JSONRPC version should be 2.0") 284 | assert.Equal(t, eventID, response.ID, "ID should match the provided request ID") 285 | 286 | // Verify result contains the same key-value pairs 287 | // JSON unmarshaling creates map[string]interface{}, so we can't use direct equality 288 | resultMap, ok := response.Result.(map[string]interface{}) 289 | assert.True(t, ok, "Result should be a map") 290 | assert.Equal(t, "value1", resultMap["key1"], "Value for key1 should match") 291 | assert.Equal(t, "value2", resultMap["key2"], "Value for key2 should match") 292 | } 293 | -------------------------------------------------------------------------------- /log/log.go: -------------------------------------------------------------------------------- 1 | // Tencent is pleased to support the open source community by making trpc-a2a-go available. 2 | // 3 | // Copyright (C) 2025 THL A29 Limited, a Tencent company. All rights reserved. 4 | // 5 | // trpc-a2a-go is licensed under the Apache License Version 2.0. 6 | 7 | // Package log provides logging utilities. 8 | package log 9 | 10 | import ( 11 | "os" 12 | 13 | "go.uber.org/zap" 14 | "go.uber.org/zap/zapcore" 15 | ) 16 | 17 | // Default borrows logging utilities from zap. 18 | // You may replace it with whatever logger you like as long as it implements log.Logger interface. 19 | var Default Logger = zap.New( 20 | zapcore.NewCore( 21 | zapcore.NewConsoleEncoder(encoderConfig), 22 | zapcore.AddSync(os.Stdout), 23 | zap.NewAtomicLevelAt(zapcore.InfoLevel), 24 | ), 25 | zap.AddCaller(), 26 | zap.AddCallerSkip(1), 27 | ).Sugar() 28 | 29 | var encoderConfig = zapcore.EncoderConfig{ 30 | TimeKey: "ts", 31 | LevelKey: "lvl", 32 | NameKey: "name", 33 | CallerKey: "caller", 34 | MessageKey: "message", 35 | StacktraceKey: "stacktrace", 36 | LineEnding: zapcore.DefaultLineEnding, 37 | EncodeLevel: zapcore.CapitalColorLevelEncoder, 38 | EncodeTime: zapcore.RFC3339TimeEncoder, 39 | EncodeDuration: zapcore.SecondsDurationEncoder, 40 | EncodeCaller: zapcore.ShortCallerEncoder, 41 | } 42 | 43 | // Logger is the underlying logging work for trpc-a2a-go. 44 | type Logger interface { 45 | // Debug logs to DEBUG log. Arguments are handled in the manner of fmt.Print. 46 | Debug(args ...interface{}) 47 | // Debugf logs to DEBUG log. Arguments are handled in the manner of fmt.Printf. 48 | Debugf(format string, args ...interface{}) 49 | // Info logs to INFO log. Arguments are handled in the manner of fmt.Print. 50 | Info(args ...interface{}) 51 | // Infof logs to INFO log. Arguments are handled in the manner of fmt.Printf. 52 | Infof(format string, args ...interface{}) 53 | // Warn logs to WARNING log. Arguments are handled in the manner of fmt.Print. 54 | Warn(args ...interface{}) 55 | // Warnf logs to WARNING log. Arguments are handled in the manner of fmt.Printf. 56 | Warnf(format string, args ...interface{}) 57 | // Error logs to ERROR log. Arguments are handled in the manner of fmt.Print. 58 | Error(args ...interface{}) 59 | // Errorf logs to ERROR log. Arguments are handled in the manner of fmt.Printf. 60 | Errorf(format string, args ...interface{}) 61 | // Fatal logs to ERROR log. Arguments are handled in the manner of fmt.Print. 62 | Fatal(args ...interface{}) 63 | // Fatalf logs to ERROR log. Arguments are handled in the manner of fmt.Printf. 64 | Fatalf(format string, args ...interface{}) 65 | } 66 | 67 | // Debug logs to DEBUG log. Arguments are handled in the manner of fmt.Print. 68 | func Debug(args ...interface{}) { 69 | Default.Debug(args...) 70 | } 71 | 72 | // Debugf logs to DEBUG log. Arguments are handled in the manner of fmt.Printf. 73 | func Debugf(format string, args ...interface{}) { 74 | Default.Debugf(format, args...) 75 | } 76 | 77 | // Info logs to INFO log. Arguments are handled in the manner of fmt.Print. 78 | func Info(args ...interface{}) { 79 | Default.Info(args...) 80 | } 81 | 82 | // Infof logs to INFO log. Arguments are handled in the manner of fmt.Printf. 83 | func Infof(format string, args ...interface{}) { 84 | Default.Infof(format, args...) 85 | } 86 | 87 | // Warn logs to WARNING log. Arguments are handled in the manner of fmt.Print. 88 | func Warn(args ...interface{}) { 89 | Default.Warn(args...) 90 | } 91 | 92 | // Warnf logs to WARNING log. Arguments are handled in the manner of fmt.Printf. 93 | func Warnf(format string, args ...interface{}) { 94 | Default.Warnf(format, args...) 95 | } 96 | 97 | // Error logs to ERROR log. Arguments are handled in the manner of fmt.Print. 98 | func Error(args ...interface{}) { 99 | Default.Error(args...) 100 | } 101 | 102 | // Errorf logs to ERROR log. Arguments are handled in the manner of fmt.Printf. 103 | func Errorf(format string, args ...interface{}) { 104 | Default.Errorf(format, args...) 105 | } 106 | 107 | // Fatal logs to ERROR log. Arguments are handled in the manner of fmt.Print. 108 | func Fatal(args ...interface{}) { 109 | Default.Fatal(args...) 110 | } 111 | 112 | // Fatalf logs to ERROR log. Arguments are handled in the manner of fmt.Printf. 113 | func Fatalf(format string, args ...interface{}) { 114 | Default.Fatalf(format, args...) 115 | } 116 | -------------------------------------------------------------------------------- /log/log_test.go: -------------------------------------------------------------------------------- 1 | // Tencent is pleased to support the open source community by making trpc-a2a-go available. 2 | // 3 | // Copyright (C) 2025 THL A29 Limited, a Tencent company. All rights reserved. 4 | // 5 | // trpc-a2a-go is licensed under the Apache License Version 2.0. 6 | 7 | package log_test 8 | 9 | import ( 10 | "testing" 11 | 12 | "trpc.group/trpc-go/trpc-a2a-go/log" 13 | ) 14 | 15 | func TestLog(t *testing.T) { 16 | log.Default = &noopLogger{} 17 | log.Debug("test") 18 | log.Debugf("test") 19 | log.Info("test") 20 | log.Infof("test") 21 | log.Warn("test") 22 | log.Warnf("test") 23 | log.Error("test") 24 | log.Errorf("test") 25 | log.Fatal("test") 26 | log.Fatalf("test") 27 | } 28 | 29 | type noopLogger struct{} 30 | 31 | func (*noopLogger) Debug(args ...interface{}) {} 32 | func (*noopLogger) Debugf(format string, args ...interface{}) {} 33 | func (*noopLogger) Info(args ...interface{}) {} 34 | func (*noopLogger) Infof(format string, args ...interface{}) {} 35 | func (*noopLogger) Warn(args ...interface{}) {} 36 | func (*noopLogger) Warnf(format string, args ...interface{}) {} 37 | func (*noopLogger) Error(args ...interface{}) {} 38 | func (*noopLogger) Errorf(format string, args ...interface{}) {} 39 | func (*noopLogger) Fatal(args ...interface{}) {} 40 | func (*noopLogger) Fatalf(format string, args ...interface{}) {} 41 | -------------------------------------------------------------------------------- /protocol/protocol.go: -------------------------------------------------------------------------------- 1 | // Tencent is pleased to support the open source community by making trpc-a2a-go available. 2 | // 3 | // Copyright (C) 2025 THL A29 Limited, a Tencent company. All rights reserved. 4 | // 5 | // trpc-a2a-go is licensed under the Apache License Version 2.0. 6 | 7 | // Package protocol defines constants and potentially shared types for the A2A protocol itself. 8 | package protocol 9 | 10 | // A2A RPC Method Names define the standard method strings used in the A2A protocol's Task Service. 11 | const ( 12 | MethodTasksSend = "tasks/send" 13 | MethodTasksSendSubscribe = "tasks/sendSubscribe" 14 | MethodTasksGet = "tasks/get" 15 | MethodTasksCancel = "tasks/cancel" 16 | MethodTasksPushNotificationSet = "tasks/pushNotification/set" 17 | MethodTasksPushNotificationGet = "tasks/pushNotification/get" 18 | MethodTasksResubscribe = "tasks/resubscribe" 19 | ) 20 | 21 | // A2A SSE Event Types define the standard event type strings used in A2A SSE streams. 22 | const ( 23 | EventTaskStatusUpdate = "task_status_update" 24 | EventTaskArtifactUpdate = "task_artifact_update" 25 | // EventClose is used internally by this implementation's server to signal stream closure. 26 | // Note: This might not be part of the formal A2A spec but is used in server logic. 27 | EventClose = "close" 28 | ) 29 | 30 | // A2A HTTP Endpoint Paths define the standard paths used in the A2A protocol. 31 | const ( 32 | // AgentCardPath is the path for the agent metadata JSON endpoint. 33 | AgentCardPath = "/.well-known/agent.json" 34 | // JWKSPath is the path for the JWKS endpoint. 35 | JWKSPath = "/.well-known/jwks.json" 36 | // DefaultJSONRPCPath is the default path for the JSON-RPC endpoint. 37 | DefaultJSONRPCPath = "/" 38 | ) 39 | -------------------------------------------------------------------------------- /protocol/protocol_test.go: -------------------------------------------------------------------------------- 1 | // Tencent is pleased to support the open source community by making trpc-a2a-go available. 2 | // 3 | // Copyright (C) 2025 THL A29 Limited, a Tencent company. All rights reserved. 4 | // 5 | // trpc-a2a-go is licensed under the Apache License Version 2.0. 6 | 7 | // Package protocol_test provides blackbox tests for the protocol package. 8 | package protocol_test 9 | 10 | import ( 11 | "testing" 12 | 13 | "github.com/stretchr/testify/assert" 14 | "trpc.group/trpc-go/trpc-a2a-go/protocol" 15 | ) 16 | 17 | // TestMethodConstants ensures that the RPC method constants are correctly defined 18 | // and maintain their expected values. 19 | func TestMethodConstants(t *testing.T) { 20 | // Test RPC method constants 21 | assert.Equal(t, "tasks/send", protocol.MethodTasksSend, 22 | "MethodTasksSend should be 'tasks/send'") 23 | assert.Equal(t, "tasks/sendSubscribe", protocol.MethodTasksSendSubscribe, 24 | "MethodTasksSendSubscribe should be 'tasks/sendSubscribe'") 25 | assert.Equal(t, "tasks/get", protocol.MethodTasksGet, 26 | "MethodTasksGet should be 'tasks/get'") 27 | assert.Equal(t, "tasks/cancel", protocol.MethodTasksCancel, 28 | "MethodTasksCancel should be 'tasks/cancel'") 29 | assert.Equal(t, "tasks/pushNotification/set", protocol.MethodTasksPushNotificationSet, 30 | "MethodTasksPushNotificationSet should be 'tasks/pushNotification/set'") 31 | assert.Equal(t, "tasks/pushNotification/get", protocol.MethodTasksPushNotificationGet, 32 | "MethodTasksPushNotificationGet should be 'tasks/pushNotification/get'") 33 | assert.Equal(t, "tasks/resubscribe", protocol.MethodTasksResubscribe, 34 | "MethodTasksResubscribe should be 'tasks/resubscribe'") 35 | } 36 | 37 | // TestEventTypeConstants ensures that the SSE event type constants are correctly defined 38 | // and maintain their expected values. 39 | func TestEventTypeConstants(t *testing.T) { 40 | // Test SSE event type constants 41 | assert.Equal(t, "task_status_update", protocol.EventTaskStatusUpdate, 42 | "EventTaskStatusUpdate should be 'task_status_update'") 43 | assert.Equal(t, "task_artifact_update", protocol.EventTaskArtifactUpdate, 44 | "EventTaskArtifactUpdate should be 'task_artifact_update'") 45 | assert.Equal(t, "close", protocol.EventClose, "EventClose should be 'close'") 46 | } 47 | 48 | // TestEndpointPathConstants ensures that the HTTP endpoint path constants are correctly defined 49 | // and maintain their expected values. 50 | func TestEndpointPathConstants(t *testing.T) { 51 | // Test HTTP endpoint path constants 52 | assert.Equal(t, "/.well-known/agent.json", protocol.AgentCardPath, 53 | "AgentCardPath should be '/.well-known/agent.json'") 54 | assert.Equal(t, "/.well-known/jwks.json", protocol.JWKSPath, "JWKSPath should be '/.well-known/jwks.json'") 55 | assert.Equal(t, "/", protocol.DefaultJSONRPCPath, "DefaultJSONRPCPath should be '/'") 56 | } 57 | 58 | // TestConstantRelationships checks relationships between related constants 59 | // to ensure protocol coherence. 60 | func TestConstantRelationships(t *testing.T) { 61 | // Test that push notification methods are properly paired 62 | assert.True(t, protocol.MethodTasksPushNotificationSet != protocol.MethodTasksPushNotificationGet, 63 | "Push notification set and get methods should be distinct") 64 | 65 | // Test that event types are distinct 66 | assert.True(t, protocol.EventTaskStatusUpdate != protocol.EventTaskArtifactUpdate, 67 | "Status and artifact event types should be distinct") 68 | assert.True(t, protocol.EventTaskStatusUpdate != protocol.EventClose, 69 | "Status update and close event types should be distinct") 70 | assert.True(t, protocol.EventTaskArtifactUpdate != protocol.EventClose, 71 | "Artifact update and close event types should be distinct") 72 | 73 | // Test that HTTP endpoint paths are distinct 74 | assert.True(t, protocol.AgentCardPath != protocol.JWKSPath, 75 | "Agent card and JWKS paths should be distinct") 76 | assert.True(t, protocol.AgentCardPath != protocol.DefaultJSONRPCPath, 77 | "Agent card and JSON-RPC paths should be distinct") 78 | assert.True(t, protocol.JWKSPath != protocol.DefaultJSONRPCPath, 79 | "JWKS and JSON-RPC paths should be distinct") 80 | } 81 | 82 | // TestConsistencyWithSpecification tests that our implementation's constants 83 | // align with the A2A protocol specification. 84 | func TestConsistencyWithSpecification(t *testing.T) { 85 | // These tests ensure that key constants follow the patterns defined in the specification 86 | 87 | // All method constants should start with "tasks/" 88 | methodConstants := []string{ 89 | protocol.MethodTasksSend, 90 | protocol.MethodTasksSendSubscribe, 91 | protocol.MethodTasksGet, 92 | protocol.MethodTasksCancel, 93 | protocol.MethodTasksPushNotificationSet, 94 | protocol.MethodTasksPushNotificationGet, 95 | protocol.MethodTasksResubscribe, 96 | } 97 | 98 | for _, method := range methodConstants { 99 | assert.True(t, len(method) >= 6 && method[0:6] == "tasks/", 100 | "Method %s should start with 'tasks/'", method) 101 | } 102 | 103 | // Push notification methods should include 'pushNotification' in the path 104 | pushNotificationMethods := []string{ 105 | protocol.MethodTasksPushNotificationSet, 106 | protocol.MethodTasksPushNotificationGet, 107 | } 108 | 109 | for _, method := range pushNotificationMethods { 110 | assert.Contains(t, method, "pushNotification", 111 | "Push notification method %s should contain 'pushNotification'", method) 112 | } 113 | 114 | // Well-known paths should start with '/.well-known/' 115 | wellKnownPaths := []string{ 116 | protocol.AgentCardPath, 117 | protocol.JWKSPath, 118 | } 119 | 120 | for _, path := range wellKnownPaths { 121 | assert.True(t, len(path) >= 13 && path[0:13] == "/.well-known/", 122 | "Well-known path %s should start with '/.well-known/'", path) 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /server/options.go: -------------------------------------------------------------------------------- 1 | // Tencent is pleased to support the open source community by making trpc-a2a-go available. 2 | // 3 | // Copyright (C) 2025 THL A29 Limited, a Tencent company. All rights reserved. 4 | // 5 | // trpc-a2a-go is licensed under the Apache License Version 2.0. 6 | 7 | package server 8 | 9 | import ( 10 | "time" 11 | 12 | "trpc.group/trpc-go/trpc-a2a-go/auth" 13 | ) 14 | 15 | const ( 16 | defaultReadTimeout = 60 * time.Second 17 | defaultWriteTimeout = 60 * time.Second 18 | defaultIdleTimeout = 300 * time.Second 19 | ) 20 | 21 | // Option is a function that configures the A2AServer. 22 | type Option func(*A2AServer) 23 | 24 | // WithCORSEnabled enables CORS for the server. 25 | func WithCORSEnabled(enabled bool) Option { 26 | return func(s *A2AServer) { 27 | s.corsEnabled = enabled 28 | } 29 | } 30 | 31 | // WithJSONRPCEndpoint sets the path for the JSON-RPC endpoint. 32 | // Default is the root path ("/"). 33 | func WithJSONRPCEndpoint(path string) Option { 34 | return func(s *A2AServer) { 35 | s.jsonRPCEndpoint = path 36 | } 37 | } 38 | 39 | // WithReadTimeout sets the read timeout for the HTTP server. 40 | func WithReadTimeout(timeout time.Duration) Option { 41 | return func(s *A2AServer) { 42 | s.readTimeout = timeout 43 | } 44 | } 45 | 46 | // WithWriteTimeout sets the write timeout for the HTTP server. 47 | func WithWriteTimeout(timeout time.Duration) Option { 48 | return func(s *A2AServer) { 49 | s.writeTimeout = timeout 50 | } 51 | } 52 | 53 | // WithIdleTimeout sets the idle timeout for the HTTP server. 54 | func WithIdleTimeout(timeout time.Duration) Option { 55 | return func(s *A2AServer) { 56 | s.idleTimeout = timeout 57 | } 58 | } 59 | 60 | // WithAuthProvider sets the authentication provider for the server. 61 | // If not set, the server will not require authentication. 62 | func WithAuthProvider(provider auth.Provider) Option { 63 | return func(s *A2AServer) { 64 | s.authProvider = provider 65 | } 66 | } 67 | 68 | // WithJWKSEndpoint enables the JWKS endpoint for push notification authentication. 69 | // This is used for providing public keys for JWT verification. 70 | // The path defaults to "/.well-known/jwks.json". 71 | func WithJWKSEndpoint(enabled bool, path string) Option { 72 | return func(s *A2AServer) { 73 | s.jwksEnabled = enabled 74 | if path != "" { 75 | s.jwksEndpoint = path 76 | } 77 | } 78 | } 79 | 80 | // WithPushNotificationAuthenticator sets a custom authenticator for push notifications. 81 | // This allows reusing the same authenticator instance throughout the application 82 | // ensuring that the same keys are used for signing and verification. 83 | func WithPushNotificationAuthenticator(authenticator *auth.PushNotificationAuthenticator) Option { 84 | return func(s *A2AServer) { 85 | s.pushAuth = authenticator 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /server/options_test.go: -------------------------------------------------------------------------------- 1 | // Tencent is pleased to support the open source community by making trpc-a2a-go available. 2 | // 3 | // Copyright (C) 2025 THL A29 Limited, a Tencent company. All rights reserved. 4 | // 5 | // trpc-a2a-go is licensed under the Apache License Version 2.0. 6 | 7 | package server 8 | 9 | import ( 10 | "net/http" 11 | "testing" 12 | "time" 13 | 14 | "github.com/stretchr/testify/assert" 15 | "github.com/stretchr/testify/require" 16 | "trpc.group/trpc-go/trpc-a2a-go/auth" 17 | ) 18 | 19 | func TestWithCORSEnabled(t *testing.T) { 20 | // Test with CORS enabled 21 | opt := WithCORSEnabled(true) 22 | s := &A2AServer{} 23 | opt(s) 24 | assert.True(t, s.corsEnabled) 25 | 26 | // Test with CORS disabled 27 | opt = WithCORSEnabled(false) 28 | opt(s) 29 | assert.False(t, s.corsEnabled) 30 | } 31 | 32 | func TestWithJSONRPCEndpoint(t *testing.T) { 33 | // Test with custom JSON-RPC path 34 | path := "/custom/path" 35 | opt := WithJSONRPCEndpoint(path) 36 | s := &A2AServer{} 37 | opt(s) 38 | assert.Equal(t, path, s.jsonRPCEndpoint) 39 | } 40 | 41 | func TestWithReadTimeout(t *testing.T) { 42 | // Test with custom read timeout 43 | timeout := 30 * time.Second 44 | opt := WithReadTimeout(timeout) 45 | s := &A2AServer{} 46 | opt(s) 47 | assert.Equal(t, timeout, s.readTimeout) 48 | } 49 | 50 | func TestWithWriteTimeout(t *testing.T) { 51 | // Test with custom write timeout 52 | timeout := 30 * time.Second 53 | opt := WithWriteTimeout(timeout) 54 | s := &A2AServer{} 55 | opt(s) 56 | assert.Equal(t, timeout, s.writeTimeout) 57 | } 58 | 59 | func TestWithIdleTimeout(t *testing.T) { 60 | // Test with custom idle timeout 61 | timeout := 120 * time.Second 62 | opt := WithIdleTimeout(timeout) 63 | s := &A2AServer{} 64 | opt(s) 65 | assert.Equal(t, timeout, s.idleTimeout) 66 | } 67 | 68 | func TestWithAuthProvider(t *testing.T) { 69 | // Create a mock auth provider 70 | provider := &mockAuthProvider{} 71 | 72 | // Test with auth provider 73 | opt := WithAuthProvider(provider) 74 | s := &A2AServer{} 75 | opt(s) 76 | assert.Equal(t, provider, s.authProvider) 77 | } 78 | 79 | func TestWithJWKSEndpoint(t *testing.T) { 80 | // Test with JWKS endpoint enabled and custom path 81 | customPath := "/custom/jwks.json" 82 | opt := WithJWKSEndpoint(true, customPath) 83 | s := &A2AServer{} 84 | opt(s) 85 | assert.True(t, s.jwksEnabled) 86 | assert.Equal(t, customPath, s.jwksEndpoint) 87 | 88 | // Test with JWKS endpoint disabled 89 | opt = WithJWKSEndpoint(false, "") 90 | opt(s) 91 | assert.False(t, s.jwksEnabled) 92 | } 93 | 94 | // Test for WithPushNotificationAuthenticator option 95 | func TestWithPushNotificationAuthenticator(t *testing.T) { 96 | authenticator := auth.NewPushNotificationAuthenticator() 97 | require.NoError(t, authenticator.GenerateKeyPair()) 98 | 99 | serverOptions := &A2AServer{} 100 | opt := WithPushNotificationAuthenticator(authenticator) 101 | opt(serverOptions) 102 | 103 | assert.Equal(t, authenticator, serverOptions.pushAuth) 104 | } 105 | 106 | // mockAuthProvider is a simple mock implementing auth.Provider interface 107 | type mockAuthProvider struct{} 108 | 109 | func (p *mockAuthProvider) Authenticate(r *http.Request) (*auth.User, error) { 110 | return &auth.User{ID: "test-user"}, nil 111 | } 112 | -------------------------------------------------------------------------------- /server/types.go: -------------------------------------------------------------------------------- 1 | // Tencent is pleased to support the open source community by making trpc-a2a-go available. 2 | // 3 | // Copyright (C) 2025 THL A29 Limited, a Tencent company. All rights reserved. 4 | // 5 | // trpc-a2a-go is licensed under the Apache License Version 2.0. 6 | 7 | // Package server contains the A2A server implementation and related types. 8 | package server 9 | 10 | import ( 11 | "trpc.group/trpc-go/trpc-a2a-go/protocol" 12 | ) 13 | 14 | // AgentCapabilities defines the capabilities supported by an agent. 15 | type AgentCapabilities struct { 16 | // Streaming is a flag indicating if the agent supports streaming responses. 17 | Streaming bool `json:"streaming"` 18 | // PushNotifications is a flag indicating if the agent can push notifications. 19 | PushNotifications bool `json:"pushNotifications"` 20 | // StateTransitionHistory is a flag indicating if the agent can provide task history. 21 | StateTransitionHistory bool `json:"stateTransitionHistory"` 22 | } 23 | 24 | // AgentSkill describes a specific capability or function of the agent. 25 | type AgentSkill struct { 26 | // ID is the unique identifier for the skill. 27 | ID string `json:"id"` 28 | // Name is the human-readable name of the skill. 29 | Name string `json:"name"` 30 | // Description is an optional detailed description of the skill. 31 | Description *string `json:"description,omitempty"` 32 | // Tags are optional tags for categorization. 33 | Tags []string `json:"tags,omitempty"` 34 | // Examples are optional usage examples. 35 | Examples []string `json:"examples,omitempty"` 36 | // InputModes are the supported input data modes/types. 37 | InputModes []string `json:"inputModes,omitempty"` 38 | // OutputModes are the supported output data modes/types. 39 | OutputModes []string `json:"outputModes,omitempty"` 40 | } 41 | 42 | // AgentProvider contains information about the agent's provider or developer. 43 | type AgentProvider struct { 44 | // Organization is the name of the provider. 45 | Organization string `json:"organization"` 46 | // URL is an optional URL for the provider. 47 | URL *string `json:"url,omitempty"` 48 | } 49 | 50 | // AgentAuthentication defines the authentication mechanism required by the agent. 51 | type AgentAuthentication struct { 52 | // Type is the type of authentication (e.g., "none", "apiKey", "oauth"). 53 | Type string `json:"type"` 54 | // Required is a flag indicating if authentication is mandatory. 55 | Required bool `json:"required"` 56 | // Config is an optional configuration details for the auth type. 57 | Config interface{} `json:"config,omitempty"` 58 | } 59 | 60 | // AgentCard is the metadata structure describing an A2A agent. 61 | // This is typically returned by the agent_get_card method. 62 | type AgentCard struct { 63 | // Name is the name of the agent. 64 | Name string `json:"name"` 65 | // Description is an optional description of the agent. 66 | Description *string `json:"description,omitempty"` 67 | // URL is the endpoint URL where the agent is hosted. 68 | URL string `json:"url"` 69 | // Provider is an optional provider information. 70 | Provider *AgentProvider `json:"provider,omitempty"` 71 | // Version is the agent version string. 72 | Version string `json:"version"` 73 | // DocumentationURL is an optional link to documentation. 74 | DocumentationURL *string `json:"documentationUrl,omitempty"` 75 | // Capabilities are the declared capabilities of the agent. 76 | Capabilities AgentCapabilities `json:"capabilities"` 77 | // Authentication is an optional authentication details. 78 | Authentication *protocol.AuthenticationInfo `json:"authentication,omitempty"` 79 | // DefaultInputModes are the default input modes if not specified per skill. 80 | DefaultInputModes []string `json:"defaultInputModes,omitempty"` 81 | // DefaultOutputModes are the default output modes if not specified per skill. 82 | DefaultOutputModes []string `json:"defaultOutputModes,omitempty"` 83 | // Skills are optional list of specific skills. 84 | Skills []AgentSkill `json:"skills"` 85 | } 86 | -------------------------------------------------------------------------------- /taskmanager/errors.go: -------------------------------------------------------------------------------- 1 | // Tencent is pleased to support the open source community by making trpc-a2a-go available. 2 | // 3 | // Copyright (C) 2025 THL A29 Limited, a Tencent company. All rights reserved. 4 | // 5 | // trpc-a2a-go is licensed under the Apache License Version 2.0. 6 | 7 | // Package taskmanager defines task management interfaces, types, and implementations. 8 | package taskmanager 9 | 10 | import ( 11 | "fmt" 12 | 13 | "trpc.group/trpc-go/trpc-a2a-go/internal/jsonrpc" 14 | "trpc.group/trpc-go/trpc-a2a-go/protocol" 15 | ) 16 | 17 | // Custom JSON-RPC error codes specific to the TaskManager. 18 | const ( 19 | ErrCodeTaskNotFound int = -32001 // Custom server error code range. 20 | ErrCodeTaskFinal int = -32002 21 | ErrCodePushNotificationNotConfigured int = -32003 22 | ) 23 | 24 | // ErrTaskNotFound creates a JSON-RPC error for task not found. 25 | // Exported function. 26 | func ErrTaskNotFound(taskID string) *jsonrpc.Error { 27 | return &jsonrpc.Error{ 28 | Code: ErrCodeTaskNotFound, 29 | Message: "Task not found", 30 | Data: fmt.Sprintf("Task with ID '%s' was not found.", taskID), 31 | } 32 | } 33 | 34 | // ErrTaskFinalState creates a JSON-RPC error for attempting an operation on a task 35 | // that is already in a final state (completed, failed, cancelled). 36 | // Exported function. 37 | func ErrTaskFinalState(taskID string, state protocol.TaskState) *jsonrpc.Error { 38 | return &jsonrpc.Error{ 39 | Code: ErrCodeTaskFinal, 40 | Message: "Task is in final state", 41 | Data: fmt.Sprintf("Task '%s' is already in final state: %s", taskID, state), 42 | } 43 | } 44 | 45 | // ErrPushNotificationNotConfigured creates a JSON-RPC error for when push notifications 46 | // haven't been configured for a task. 47 | // Exported function. 48 | func ErrPushNotificationNotConfigured(taskID string) *jsonrpc.Error { 49 | return &jsonrpc.Error{ 50 | Code: ErrCodePushNotificationNotConfigured, 51 | Message: "Push Notification not configured", 52 | Data: fmt.Sprintf("Task '%s' does not have push notifications configured.", taskID), 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /taskmanager/interface.go: -------------------------------------------------------------------------------- 1 | // Tencent is pleased to support the open source community by making trpc-a2a-go available. 2 | // 3 | // Copyright (C) 2025 THL A29 Limited, a Tencent company. All rights reserved. 4 | // 5 | // trpc-a2a-go is licensed under the Apache License Version 2.0. 6 | 7 | // Package taskmanager defines interfaces and implementations for managing A2A task lifecycles. 8 | package taskmanager 9 | 10 | import ( 11 | "context" 12 | 13 | "trpc.group/trpc-go/trpc-a2a-go/protocol" 14 | ) 15 | 16 | // TaskHandle provides methods for the agent logic (TaskProcessor) to interact 17 | // with the task manager during processing. It encapsulates the necessary callbacks. 18 | type TaskHandle interface { 19 | // UpdateStatus updates the task's state and optional message. 20 | // Returns an error if the task cannot be found or updated. 21 | UpdateStatus(state protocol.TaskState, msg *protocol.Message) error 22 | 23 | // AddArtifact adds a new artifact to the task. 24 | // Returns an error if the task cannot be found or updated. 25 | AddArtifact(artifact protocol.Artifact) error 26 | 27 | // IsStreamingRequest returns true if the task was initiated via a streaming request 28 | // (OnSendTaskSubscribe) rather than a synchronous request (OnSendTask). 29 | // This allows the TaskProcessor to adapt its behavior based on the request type. 30 | IsStreamingRequest() bool 31 | 32 | // GetSessionID returns the session ID for the task. 33 | // If the task is not associated with a session, it returns nil. 34 | GetSessionID() *string 35 | } 36 | 37 | // TaskProcessor defines the interface for the core agent logic that processes a task. 38 | // Implementations of this interface are injected into a TaskManager. 39 | type TaskProcessor interface { 40 | // Process executes the specific logic for a task. 41 | // It receives the task ID, the initial message, and a TaskHandle for callbacks. 42 | // It should use handle.Context() to check for cancellation. 43 | // It should report progress and results via handle.UpdateStatus and handle.AddArtifact. 44 | // Returning an error indicates the processing failed fundamentally. 45 | Process(ctx context.Context, taskID string, initialMsg protocol.Message, handle TaskHandle) error 46 | } 47 | 48 | // TaskProcessorWithStatusUpdate is an optional interface that can be implemented by TaskProcessor 49 | // to receive notifications when the task status changes. 50 | type TaskProcessorWithStatusUpdate interface { 51 | TaskProcessor 52 | // OnTaskStatusUpdate is called when the task status changes. 53 | // It receives the task ID, the new state, and the optional message. 54 | // It should return an error if the status update fails. 55 | OnTaskStatusUpdate(ctx context.Context, taskID string, state protocol.TaskState, message *protocol.Message) error 56 | } 57 | 58 | // TaskManager defines the interface for managing A2A task lifecycles based on the protocol. 59 | // Implementations handle task creation, updates, retrieval, cancellation, and events, 60 | // delegating the actual processing logic to an injected TaskProcessor. 61 | // This interface corresponds to the Task Service defined in the A2A Specification. 62 | // Exported interface. 63 | type TaskManager interface { 64 | // OnSendTask handles a request corresponding to the 'tasks/send' RPC method. 65 | // It creates and potentially starts processing a new task via the TaskProcessor. 66 | // It returns the initial state of the task, possibly reflecting immediate processing results. 67 | OnSendTask(ctx context.Context, request protocol.SendTaskParams) (*protocol.Task, error) 68 | 69 | // OnSendTaskSubscribe handles a request corresponding to the 'tasks/sendSubscribe' RPC method. 70 | // It creates a new task and returns a channel for receiving TaskEvent updates (streaming). 71 | // It initiates asynchronous processing via the TaskProcessor. 72 | // The channel will be closed when the task reaches a final state or an error occurs during setup/processing. 73 | OnSendTaskSubscribe(ctx context.Context, request protocol.SendTaskParams) (<-chan protocol.TaskEvent, error) 74 | 75 | // OnGetTask handles a request corresponding to the 'tasks/get' RPC method. 76 | // It retrieves the current state of an existing task. 77 | OnGetTask(ctx context.Context, params protocol.TaskQueryParams) (*protocol.Task, error) 78 | 79 | // OnCancelTask handles a request corresponding to the 'tasks/cancel' RPC method. 80 | // It requests the cancellation of an ongoing task. 81 | // This typically involves canceling the context passed to the TaskProcessor. 82 | // It returns the task state after the cancellation attempt. 83 | OnCancelTask(ctx context.Context, params protocol.TaskIDParams) (*protocol.Task, error) 84 | 85 | // OnPushNotificationSet handles a request corresponding to the 'tasks/pushNotification/set' RPC method. 86 | // It configures push notifications for a specific task. 87 | OnPushNotificationSet(ctx context.Context, params protocol.TaskPushNotificationConfig) (*protocol.TaskPushNotificationConfig, error) 88 | 89 | // OnPushNotificationGet handles a request corresponding to the 'tasks/pushNotification/get' RPC method. 90 | // It retrieves the current push notification configuration for a task. 91 | OnPushNotificationGet(ctx context.Context, params protocol.TaskIDParams) (*protocol.TaskPushNotificationConfig, error) 92 | 93 | // OnResubscribe handles a request corresponding to the 'tasks/resubscribe' RPC method. 94 | // It reestablishes an SSE stream for an existing task. 95 | OnResubscribe(ctx context.Context, params protocol.TaskIDParams) (<-chan protocol.TaskEvent, error) 96 | } 97 | -------------------------------------------------------------------------------- /taskmanager/redis/README.md: -------------------------------------------------------------------------------- 1 | # Redis Task Manager for A2A 2 | 3 | This package provides a Redis-based implementation of the A2A TaskManager interface, allowing for persistent storage of tasks and messages using Redis. 4 | 5 | ## Features 6 | 7 | - Persistent storage of tasks and task history 8 | - Support for all TaskManager operations (send task, subscribe, cancel, etc.) 9 | - Configurable key expiration time 10 | - Compatible with Redis clusters, sentinel, and standalone configurations 11 | - Thread-safe implementation 12 | - Graceful cleanup of resources 13 | 14 | ## Requirements 15 | 16 | - Go 1.21 or later 17 | - Redis 6.0 or later (recommended) 18 | - github.com/redis/go-redis/v9 library 19 | 20 | ## Installation 21 | 22 | ```bash 23 | go get trpc.group/trpc-go/trpc-a2a-go/taskmanager/redis 24 | ``` 25 | 26 | ## Usage 27 | 28 | ### Basic Usage 29 | 30 | ```go 31 | import ( 32 | "context" 33 | "log" 34 | "time" 35 | 36 | "github.com/redis/go-redis/v9" 37 | "trpc.group/trpc-go/trpc-a2a-go/taskmanager" 38 | redismgr "trpc.group/trpc-go/trpc-a2a-go/taskmanager/redis" 39 | ) 40 | 41 | func main() { 42 | // Create your task processor implementation. 43 | processor := &MyTaskProcessor{} 44 | 45 | // Configure Redis connection. 46 | redisOptions := &redis.UniversalOptions{ 47 | Addrs: []string{"localhost:6379"}, 48 | Password: "", // no password 49 | DB: 0, // use default DB 50 | } 51 | 52 | // Create Redis task manager. 53 | manager, err := redismgr.NewRedisTaskManager(processor, redismgr.Options{ 54 | RedisOptions: redisOptions, 55 | }) 56 | if err != nil { 57 | log.Fatalf("Failed to create Redis task manager: %v", err) 58 | } 59 | defer manager.Close() 60 | 61 | // Use the task manager... 62 | } 63 | ``` 64 | 65 | ### Configuring Key Expiration 66 | 67 | By default, task and message data in Redis will expire after 30 days. You can customize this: 68 | 69 | ```go 70 | // Set custom expiration time. 71 | expiration := 7 * 24 * time.Hour // 7 days 72 | 73 | manager, err := redismgr.NewRedisTaskManager(processor, redismgr.Options{ 74 | RedisOptions: redisOptions, 75 | Expiration: &expiration, 76 | }) 77 | ``` 78 | 79 | ### Using with Redis Cluster 80 | 81 | ```go 82 | redisOptions := &redis.UniversalOptions{ 83 | Addrs: []string{ 84 | "redis-node-1:6379", 85 | "redis-node-2:6379", 86 | "redis-node-3:6379", 87 | }, 88 | RouteByLatency: true, 89 | } 90 | 91 | manager, err := redismgr.NewRedisTaskManager(processor, redismgr.Options{ 92 | RedisOptions: redisOptions, 93 | }) 94 | ``` 95 | 96 | ### Using with Redis Sentinel 97 | 98 | ```go 99 | redisOptions := &redis.UniversalOptions{ 100 | Addrs: []string{"sentinel-1:26379", "sentinel-2:26379"}, 101 | MasterName: "mymaster", 102 | } 103 | 104 | manager, err := redismgr.NewRedisTaskManager(processor, redismgr.Options{ 105 | RedisOptions: redisOptions, 106 | }) 107 | ``` 108 | 109 | ## Implementation Details 110 | 111 | ### Redis Key Prefixes 112 | 113 | The implementation uses the following key patterns in Redis: 114 | 115 | - `task:ID` - Stores the serialized Task object 116 | - `msg:ID` - Stores the message history as a Redis list 117 | - `push:ID` - Stores push notification configuration 118 | 119 | ### Task Subscribers 120 | 121 | While tasks and messages are stored in Redis, subscribers for streaming updates are maintained in memory. If your application requires distributed subscription handling, consider implementing a custom solution using Redis Pub/Sub. 122 | 123 | ## Testing 124 | 125 | The package includes comprehensive tests that use an in-memory Redis server for testing. To run the tests: 126 | 127 | ```bash 128 | go test -v 129 | ``` 130 | 131 | For end-to-end testing, the package uses [miniredis](https://github.com/alicebob/miniredis), which provides a fully featured in-memory Redis implementation perfect for testing without external dependencies. 132 | 133 | ## Full Example 134 | 135 | See the [example directory](./example) for a complete working example. 136 | 137 | ## License 138 | 139 | This package is part of the A2A Go implementation and follows the same license. 140 | -------------------------------------------------------------------------------- /taskmanager/redis/example/README.md: -------------------------------------------------------------------------------- 1 | # A2A Redis Task Manager Example 2 | 3 | This example demonstrates how to create a complete A2A (Application-to-Application) flow using the Redis task manager. It includes both a server and client implementation that use the official A2A Go packages. 4 | 5 | ## Prerequisites 6 | 7 | - Go 1.21 or later 8 | - Redis 6.0 or later running locally (or accessible through network) 9 | 10 | ## Running the Server 11 | 12 | The server implements a simple task processor that can receive tasks, process them, and return results. It uses Redis for task storage and state management. 13 | 14 | ```bash 15 | # Start Redis if it's not already running 16 | # For example, using Docker: 17 | docker run --name redis -p 6379:6379 -d redis 18 | 19 | # Run the server with default settings (Redis on localhost:6379) 20 | cd server 21 | go run main.go 22 | 23 | # Or with custom settings 24 | go run main.go -port=8080 -redis=localhost:6379 -redis-pass="" -redis-db=0 25 | ``` 26 | 27 | The server supports the following command-line arguments: 28 | 29 | - `-port`: The HTTP port to listen on (default: 8080) 30 | - `-redis`: Redis server address (default: localhost:6379) 31 | - `-redis-pass`: Redis password (default: "") 32 | - `-redis-db`: Redis database number (default: 0) 33 | 34 | ## Running the Client 35 | 36 | The client provides a command-line interface to interact with the server. It supports sending tasks, retrieving task status, and streaming task updates. 37 | 38 | ```bash 39 | cd client 40 | go run main.go -op=send -message="Hello, world!" 41 | 42 | # Get task status 43 | go run main.go -op=get -task= 44 | 45 | # Cancel a task 46 | go run main.go -op=cancel -task= 47 | 48 | # Stream task updates 49 | go run main.go -op=stream -message="Hello, streaming!" 50 | ``` 51 | 52 | The client supports the following operations: 53 | 54 | - `send`: Create and send a new task, then poll until completion 55 | - `get`: Retrieve the status of an existing task 56 | - `cancel`: Cancel an in-progress task 57 | - `stream`: Create a task and stream updates until completion 58 | 59 | Command-line arguments: 60 | 61 | - `-server`: The A2A server URL (default: http://localhost:8080) 62 | - `-message`: The message to send (default: "Hello, world!") 63 | - `-op`: The operation to perform (default: "send") 64 | - `-task`: The task ID (required for get and cancel operations) 65 | - `-idkey`: An idempotency key for task creation (optional) 66 | 67 | ## Understanding the Code 68 | 69 | ### Server 70 | 71 | The server implementation: 72 | 73 | 1. Uses the `redismgr.TaskManager` for persistent task storage in Redis 74 | 2. Implements a custom `DemoTaskProcessor` for task processing logic 75 | 3. Creates an official A2A server with appropriate configuration 76 | 4. Handles tasks via the A2A protocol endpoints 77 | 78 | ### Client 79 | 80 | The client implementation: 81 | 82 | 1. Uses the official `client.A2AClient` to communicate with the server 83 | 2. Supports various operations through the command-line interface 84 | 3. Formats and displays task state and artifacts 85 | 4. Demonstrates both synchronous and streaming interaction 86 | 87 | ## Example Flow 88 | 89 | 1. Start the server: 90 | ```bash 91 | cd server 92 | go run main.go 93 | ``` 94 | 95 | 2. Send a task from the client: 96 | ```bash 97 | cd client 98 | go run main.go -op=send -message="Process this message" 99 | ``` 100 | 101 | 3. The client will display task status updates, including the final result and any artifacts produced. 102 | 103 | ## Error Handling 104 | 105 | The example demonstrates error handling in several ways: 106 | 107 | - If you include the word "error" in your message, the server will return a simulated error 108 | - Connection errors between client and server are properly reported 109 | - Task cancellation is supported through the cancel operation 110 | 111 | ## Next Steps 112 | 113 | - Modify the `DemoTaskProcessor` to implement your own task processing logic 114 | - Configure the Redis task manager for production use (authentication, clustering, etc.) 115 | - Integrate the server into your own application -------------------------------------------------------------------------------- /taskmanager/redis/example/server/main.go: -------------------------------------------------------------------------------- 1 | // Tencent is pleased to support the open source community by making trpc-a2a-go available. 2 | // 3 | // Copyright (C) 2025 THL A29 Limited, a Tencent company. All rights reserved. 4 | // 5 | // trpc-a2a-go is licensed under the Apache License Version 2.0. 6 | 7 | // Package main provides a simple A2A server using the Redis task manager. 8 | package main 9 | 10 | import ( 11 | "context" 12 | "flag" 13 | "fmt" 14 | "log" 15 | "strings" 16 | "time" 17 | 18 | "github.com/redis/go-redis/v9" 19 | 20 | "trpc.group/trpc-go/trpc-a2a-go/protocol" 21 | "trpc.group/trpc-go/trpc-a2a-go/server" 22 | "trpc.group/trpc-go/trpc-a2a-go/taskmanager" 23 | redismgr "trpc.group/trpc-go/trpc-a2a-go/taskmanager/redis" 24 | ) 25 | 26 | // DemoTaskProcessor implements TaskProcessor for our demo server. 27 | type DemoTaskProcessor struct{} 28 | 29 | // Process implements the task processing logic. 30 | func (p *DemoTaskProcessor) Process( 31 | ctx context.Context, 32 | taskID string, 33 | initialMsg protocol.Message, 34 | handle taskmanager.TaskHandle, 35 | ) error { 36 | // First, update the status to show we're working 37 | if err := handle.UpdateStatus(protocol.TaskStateWorking, nil); err != nil { 38 | return fmt.Errorf("failed to update status to working: %w", err) 39 | } 40 | 41 | // Extract the user message 42 | var userMessage string 43 | if len(initialMsg.Parts) > 0 { 44 | if textPart, ok := initialMsg.Parts[0].(protocol.TextPart); ok { 45 | userMessage = textPart.Text 46 | } 47 | } 48 | 49 | // Log the task 50 | log.Printf("Processing task %s: %s", taskID, userMessage) 51 | 52 | // Simulate some work 53 | select { 54 | case <-ctx.Done(): 55 | return ctx.Err() 56 | case <-time.After(1 * time.Second): 57 | // Processed 58 | } 59 | 60 | // Create a response based on the user message 61 | response := fmt.Sprintf("Processed: %s", userMessage) 62 | if strings.Contains(strings.ToLower(userMessage), "error") { 63 | return fmt.Errorf("simulated error requested in message") 64 | } 65 | 66 | // Add an artifact 67 | artifact := protocol.Artifact{ 68 | Name: strPtr("result"), 69 | Parts: []protocol.Part{protocol.NewTextPart(response)}, 70 | Index: 0, 71 | } 72 | lastChunk := true 73 | artifact.LastChunk = &lastChunk 74 | 75 | if err := handle.AddArtifact(artifact); err != nil { 76 | return fmt.Errorf("failed to add artifact: %w", err) 77 | } 78 | 79 | // Complete with a success message 80 | successMsg := &protocol.Message{ 81 | Role: protocol.MessageRoleAgent, 82 | Parts: []protocol.Part{ 83 | protocol.NewTextPart(fmt.Sprintf("Task completed: %s", userMessage)), 84 | }, 85 | } 86 | if err := handle.UpdateStatus(protocol.TaskStateCompleted, successMsg); err != nil { 87 | return fmt.Errorf("failed to update final status: %w", err) 88 | } 89 | 90 | return nil 91 | } 92 | 93 | // Helper function to create string pointers 94 | func strPtr(s string) *string { 95 | return &s 96 | } 97 | 98 | func main() { 99 | // Parse command line flags 100 | port := flag.Int("port", 8080, "Server port") 101 | redisAddr := flag.String("redis", "localhost:6379", "Redis server address") 102 | redisPassword := flag.String("redis-pass", "", "Redis password") 103 | redisDB := flag.Int("redis-db", 0, "Redis database") 104 | flag.Parse() 105 | 106 | log.Printf("Starting A2A server with Redis task manager on port %d", *port) 107 | log.Printf("Using Redis at %s (DB: %d)", *redisAddr, *redisDB) 108 | 109 | // Create a task processor 110 | processor := &DemoTaskProcessor{} 111 | 112 | // Configure Redis client 113 | redisOptions := &redis.UniversalOptions{ 114 | Addrs: []string{*redisAddr}, 115 | Password: *redisPassword, 116 | DB: *redisDB, 117 | } 118 | 119 | // Create Redis task manager 120 | manager, err := redismgr.NewRedisTaskManager( 121 | redis.NewUniversalClient(redisOptions), 122 | processor, 123 | ) 124 | if err != nil { 125 | log.Fatalf("Failed to create Redis task manager: %v", err) 126 | } 127 | defer manager.Close() 128 | 129 | // Define the agent card with server metadata 130 | description := "A simple A2A demo server using Redis task manager" 131 | serverURL := fmt.Sprintf("http://localhost:%d", *port) 132 | version := "1.0.0" 133 | 134 | agentCard := server.AgentCard{ 135 | Name: "Redis Task Manager Demo", 136 | Description: &description, 137 | URL: serverURL, 138 | Version: version, 139 | Capabilities: server.AgentCapabilities{ 140 | Streaming: true, 141 | PushNotifications: false, 142 | StateTransitionHistory: true, 143 | }, 144 | DefaultInputModes: []string{string(protocol.PartTypeText)}, 145 | DefaultOutputModes: []string{string(protocol.PartTypeText)}, 146 | Skills: []server.AgentSkill{}, // No specific skills 147 | } 148 | 149 | // Create A2A server using the official server package 150 | a2aServer, err := server.NewA2AServer(agentCard, manager, server.WithCORSEnabled(true)) 151 | if err != nil { 152 | log.Fatalf("Failed to create A2A server: %v", err) 153 | } 154 | 155 | a2aServer.Start(fmt.Sprintf(":%d", *port)) 156 | } 157 | -------------------------------------------------------------------------------- /taskmanager/redis/go.mod: -------------------------------------------------------------------------------- 1 | module trpc.group/trpc-go/trpc-a2a-go/taskmanager/redis 2 | 3 | go 1.23.0 4 | 5 | toolchain go1.23.7 6 | 7 | replace trpc.group/trpc-go/trpc-a2a-go => ../../ 8 | 9 | require ( 10 | github.com/alicebob/miniredis/v2 v2.31.1 11 | github.com/redis/go-redis/v9 v9.7.3 12 | github.com/stretchr/testify v1.10.0 13 | trpc.group/trpc-go/trpc-a2a-go v0.0.0-00010101000000-000000000000 14 | ) 15 | 16 | require ( 17 | github.com/alicebob/gopher-json v0.0.0-20230218143504-906a9b012302 // indirect 18 | github.com/cespare/xxhash/v2 v2.2.0 // indirect 19 | github.com/davecgh/go-spew v1.1.1 // indirect 20 | github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect 21 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect 22 | github.com/goccy/go-json v0.10.3 // indirect 23 | github.com/golang-jwt/jwt/v5 v5.2.2 // indirect 24 | github.com/lestrrat-go/blackmagic v1.0.2 // indirect 25 | github.com/lestrrat-go/httpcc v1.0.1 // indirect 26 | github.com/lestrrat-go/httprc v1.0.6 // indirect 27 | github.com/lestrrat-go/iter v1.0.2 // indirect 28 | github.com/lestrrat-go/jwx/v2 v2.1.4 // indirect 29 | github.com/lestrrat-go/option v1.0.1 // indirect 30 | github.com/pmezard/go-difflib v1.0.0 // indirect 31 | github.com/segmentio/asm v1.2.0 // indirect 32 | github.com/yuin/gopher-lua v1.1.0 // indirect 33 | go.uber.org/multierr v1.10.0 // indirect 34 | go.uber.org/zap v1.27.0 // indirect 35 | golang.org/x/crypto v0.35.0 // indirect 36 | golang.org/x/oauth2 v0.29.0 // indirect 37 | golang.org/x/sys v0.30.0 // indirect 38 | gopkg.in/yaml.v3 v3.0.1 // indirect 39 | ) 40 | -------------------------------------------------------------------------------- /taskmanager/redis/go.sum: -------------------------------------------------------------------------------- 1 | github.com/DmitriyVTitov/size v1.5.0/go.mod h1:le6rNI4CoLQV1b9gzp1+3d7hMAD/uu2QcJ+aYbNgiU0= 2 | github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc= 3 | github.com/alicebob/gopher-json v0.0.0-20230218143504-906a9b012302 h1:uvdUDbHQHO85qeSydJtItA4T55Pw6BtAejd0APRJOCE= 4 | github.com/alicebob/gopher-json v0.0.0-20230218143504-906a9b012302/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc= 5 | github.com/alicebob/miniredis/v2 v2.31.1 h1:7XAt0uUg3DtwEKW5ZAGa+K7FZV2DdKQo5K/6TTnfX8Y= 6 | github.com/alicebob/miniredis/v2 v2.31.1/go.mod h1:UB/T2Uztp7MlFSDakaX1sTXUv5CASoprx0wulRT6HBg= 7 | github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= 8 | github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= 9 | github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= 10 | github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= 11 | github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= 12 | github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 13 | github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= 14 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= 15 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= 16 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 17 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 18 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 19 | github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc= 20 | github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40= 21 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= 22 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= 23 | github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA= 24 | github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= 25 | github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= 26 | github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= 27 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 28 | github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= 29 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 30 | github.com/lestrrat-go/blackmagic v1.0.2 h1:Cg2gVSc9h7sz9NOByczrbUvLopQmXrfFx//N+AkAr5k= 31 | github.com/lestrrat-go/blackmagic v1.0.2/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU= 32 | github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE= 33 | github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E= 34 | github.com/lestrrat-go/httprc v1.0.6 h1:qgmgIRhpvBqexMJjA/PmwSvhNk679oqD1RbovdCGW8k= 35 | github.com/lestrrat-go/httprc v1.0.6/go.mod h1:mwwz3JMTPBjHUkkDv/IGJ39aALInZLrhBp0X7KGUZlo= 36 | github.com/lestrrat-go/iter v1.0.2 h1:gMXo1q4c2pHmC3dn8LzRhJfP1ceCbgSiT9lUydIzltI= 37 | github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4= 38 | github.com/lestrrat-go/jwx/v2 v2.1.4 h1:uBCMmJX8oRZStmKuMMOFb0Yh9xmEMgNJLgjuKKt4/qc= 39 | github.com/lestrrat-go/jwx/v2 v2.1.4/go.mod h1:nWRbDFR1ALG2Z6GJbBXzfQaYyvn751KuuyySN2yR6is= 40 | github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU= 41 | github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= 42 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 43 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 44 | github.com/redis/go-redis/v9 v9.7.3 h1:YpPyAayJV+XErNsatSElgRZZVCwXX9QzkKYNvO7x0wM= 45 | github.com/redis/go-redis/v9 v9.7.3/go.mod h1:bGUrSggJ9X9GUmZpZNEOQKaANxSGgOEBRltRTZHSvrA= 46 | github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys= 47 | github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= 48 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 49 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 50 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 51 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 52 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 53 | github.com/yuin/gopher-lua v1.1.0 h1:BojcDhfyDWgU2f2TOzYK/g5p2gxMrku8oupLDqlnSqE= 54 | github.com/yuin/gopher-lua v1.1.0/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw= 55 | go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= 56 | go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= 57 | go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= 58 | go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= 59 | go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= 60 | go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= 61 | golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs= 62 | golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ= 63 | golang.org/x/oauth2 v0.29.0 h1:WdYw2tdTK1S8olAzWHdgeqfy+Mtm9XNhv/xJsY65d98= 64 | golang.org/x/oauth2 v0.29.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= 65 | golang.org/x/sys v0.0.0-20190204203706-41f3e6584952/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 66 | golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= 67 | golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 68 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 69 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 70 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 71 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 72 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 73 | -------------------------------------------------------------------------------- /taskmanager/redis/options.go: -------------------------------------------------------------------------------- 1 | // Tencent is pleased to support the open source community by making trpc-a2a-go available. 2 | // 3 | // Copyright (C) 2025 THL A29 Limited, a Tencent company. All rights reserved. 4 | // 5 | // trpc-a2a-go is licensed under the Apache License Version 2.0. 6 | 7 | package redis 8 | 9 | import ( 10 | "time" 11 | ) 12 | 13 | // Option is a function that configures the RedisTaskManager. 14 | type Option func(*TaskManager) 15 | 16 | // WithExpiration sets the expiration time for Redis keys. 17 | func WithExpiration(expiration time.Duration) Option { 18 | return func(o *TaskManager) { 19 | o.expiration = expiration 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /taskmanager/redis/push_notification.go: -------------------------------------------------------------------------------- 1 | // Tencent is pleased to support the open source community by making trpc-a2a-go available. 2 | // 3 | // Copyright (C) 2025 THL A29 Limited, a Tencent company. All rights reserved. 4 | // 5 | // trpc-a2a-go is licensed under the Apache License Version 2.0. 6 | 7 | // Package redis provides a Redis-based implementation of the A2A TaskManager interface. 8 | package redis 9 | 10 | import ( 11 | "bytes" 12 | "context" 13 | "encoding/json" 14 | "fmt" 15 | "io" 16 | "net/http" 17 | "strings" 18 | "time" 19 | 20 | "trpc.group/trpc-go/trpc-a2a-go/auth" 21 | "trpc.group/trpc-go/trpc-a2a-go/log" 22 | "trpc.group/trpc-go/trpc-a2a-go/protocol" 23 | ) 24 | 25 | // getPushNotificationConfig retrieves a push notification configuration for a task. 26 | func (m *TaskManager) getPushNotificationConfig( 27 | ctx context.Context, taskID string, 28 | ) (*protocol.PushNotificationConfig, error) { 29 | key := pushNotificationPrefix + taskID 30 | val, err := m.client.Get(ctx, key).Result() 31 | if err != nil { 32 | if err.Error() == "redis: nil" { 33 | // No push notification configured for this task. 34 | return nil, nil 35 | } 36 | return nil, err 37 | } 38 | var config protocol.PushNotificationConfig 39 | if err := json.Unmarshal([]byte(val), &config); err != nil { 40 | return nil, fmt.Errorf("failed to unmarshal push notification config: %w", err) 41 | } 42 | return &config, nil 43 | } 44 | 45 | // sendPushNotification sends a notification to the registered webhook URL. 46 | func (m *TaskManager) sendPushNotification( 47 | ctx context.Context, taskID string, event protocol.TaskEvent, 48 | ) error { 49 | // Get the notification config. 50 | config, err := m.getPushNotificationConfig(ctx, taskID) 51 | if err != nil { 52 | return fmt.Errorf("failed to get push notification config: %w", err) 53 | } 54 | if config == nil { 55 | // No push notification configured, nothing to do. 56 | return nil 57 | } 58 | 59 | // Prepare the notification payload. 60 | eventType := "" 61 | if _, isStatus := event.(protocol.TaskStatusUpdateEvent); isStatus { 62 | eventType = protocol.EventTaskStatusUpdate 63 | } else if _, isArtifact := event.(protocol.TaskArtifactUpdateEvent); isArtifact { 64 | eventType = protocol.EventTaskArtifactUpdate 65 | } else { 66 | return fmt.Errorf("unsupported event type: %T", event) 67 | } 68 | 69 | notification := map[string]interface{}{ 70 | "jsonrpc": "2.0", 71 | "method": "tasks/notifyEvent", 72 | "params": map[string]interface{}{ 73 | "id": taskID, 74 | "eventType": eventType, 75 | "event": event, 76 | }, 77 | } 78 | 79 | // Marshal the notification to JSON. 80 | body, err := json.Marshal(notification) 81 | if err != nil { 82 | return fmt.Errorf("failed to marshal notification: %w", err) 83 | } 84 | 85 | // Create HTTP request. 86 | req, err := http.NewRequestWithContext( 87 | ctx, http.MethodPost, config.URL, bytes.NewReader(body), 88 | ) 89 | if err != nil { 90 | return fmt.Errorf("failed to create notification request: %w", err) 91 | } 92 | req.Header.Set("Content-Type", "application/json") 93 | 94 | // Add authentication if configured. 95 | if config.Authentication != nil { 96 | // Check for JWT authentication using the "bearer" scheme. 97 | for _, scheme := range config.Authentication.Schemes { 98 | if strings.EqualFold(scheme, "bearer") { 99 | // Check if we have a JWKs URL in the metadata for JWT auth 100 | if config.Metadata != nil { 101 | if jwksURL, ok := config.Metadata["jwksUrl"].(string); ok && jwksURL != "" { 102 | // Create a JWT token for the notification 103 | if authHeader, err := m.createJWTAuthHeader(body, jwksURL); err == nil { 104 | req.Header.Set("Authorization", authHeader) 105 | break 106 | } else { 107 | log.Errorf("Failed to create JWT auth header: %v", err) 108 | } 109 | } 110 | } 111 | } 112 | } 113 | 114 | // Add token if provided. 115 | if config.Token != "" { 116 | req.Header.Set("Authorization", "Bearer "+config.Token) 117 | } 118 | } 119 | 120 | // Send the notification. 121 | client := &http.Client{Timeout: 10 * time.Second} 122 | resp, err := client.Do(req) 123 | if err != nil { 124 | return fmt.Errorf("failed to send notification: %w", err) 125 | } 126 | defer resp.Body.Close() 127 | 128 | // Check for success. 129 | if resp.StatusCode < 200 || resp.StatusCode >= 300 { 130 | body, _ := io.ReadAll(resp.Body) 131 | return fmt.Errorf("notification failed with status %d: %s", resp.StatusCode, string(body)) 132 | } 133 | 134 | return nil 135 | } 136 | 137 | // createJWTAuthHeader creates a JWT authorization header for push notifications. 138 | // It uses the jwksURL to configure a client to fetch the JWKs when needed. 139 | func (m *TaskManager) createJWTAuthHeader(payload []byte, jwksURL string) (string, error) { 140 | // Initialize the push auth helper if needed 141 | m.pushAuthMu.Lock() 142 | defer m.pushAuthMu.Unlock() 143 | 144 | if m.pushAuth == nil { 145 | // This is the first request, so initialize the push auth helper 146 | if err := m.initializePushAuth(jwksURL); err != nil { 147 | return "", err 148 | } 149 | } 150 | 151 | // Create the authorization header 152 | return m.pushAuth.CreateAuthorizationHeader(payload) 153 | } 154 | 155 | // Initialize the push notification authenticator. 156 | func (m *TaskManager) initializePushAuth(jwksURL string) error { 157 | // Create a new authenticator and generate a key pair 158 | m.pushAuth = auth.NewPushNotificationAuthenticator() 159 | if err := m.pushAuth.GenerateKeyPair(); err != nil { 160 | m.pushAuth = nil 161 | return fmt.Errorf("failed to generate key pair: %w", err) 162 | } 163 | 164 | return nil 165 | } 166 | -------------------------------------------------------------------------------- /taskmanager/task.go: -------------------------------------------------------------------------------- 1 | // Tencent is pleased to support the open source community by making trpc-a2a-go available. 2 | // 3 | // Copyright (C) 2025 THL A29 Limited, a Tencent company. All rights reserved. 4 | // 5 | // trpc-a2a-go is licensed under the Apache License Version 2.0. 6 | 7 | package taskmanager 8 | 9 | import ( 10 | "context" 11 | 12 | "trpc.group/trpc-go/trpc-a2a-go/protocol" 13 | ) 14 | 15 | // memoryTaskHandle implements the TaskHandle interface, providing callbacks 16 | // for a specific task being processed by a TaskProcessor. 17 | // It holds a reference back to the MemoryTaskManager. 18 | type memoryTaskHandle struct { 19 | taskID string 20 | manager *MemoryTaskManager 21 | } 22 | 23 | // UpdateStatus implements TaskHandle. 24 | func (h *memoryTaskHandle) UpdateStatus( 25 | state protocol.TaskState, 26 | msg *protocol.Message, 27 | ) error { 28 | return h.manager.UpdateTaskStatus(context.Background(), h.taskID, state, msg) 29 | } 30 | 31 | // AddArtifact implements TaskHandle. 32 | func (h *memoryTaskHandle) AddArtifact(artifact protocol.Artifact) error { 33 | return h.manager.AddArtifact(h.taskID, artifact) 34 | } 35 | 36 | // IsStreamingRequest checks if this task was initiated with a streaming request (OnSendTaskSubscribe). 37 | // It returns true if there are active subscribers for this task, indicating it was initiated 38 | // with OnSendTaskSubscribe rather than OnSendTask. 39 | func (h *memoryTaskHandle) IsStreamingRequest() bool { 40 | h.manager.SubMutex.RLock() 41 | defer h.manager.SubMutex.RUnlock() 42 | 43 | subscribers, exists := h.manager.Subscribers[h.taskID] 44 | return exists && len(subscribers) > 0 45 | } 46 | 47 | // GetSessionID implements TaskHandle. 48 | func (h *memoryTaskHandle) GetSessionID() *string { 49 | h.manager.SubMutex.RLock() 50 | defer h.manager.SubMutex.RUnlock() 51 | 52 | task, exists := h.manager.Tasks[h.taskID] 53 | if !exists { 54 | return nil 55 | } 56 | 57 | return task.SessionID 58 | } 59 | 60 | // isFinalState checks if a TaskState represents a terminal state. 61 | // Not exported as it's an internal helper. 62 | func isFinalState(state protocol.TaskState) bool { 63 | return state == protocol.TaskStateCompleted || state == protocol.TaskStateFailed || state == protocol.TaskStateCanceled 64 | } 65 | --------------------------------------------------------------------------------