Langertha-Knarr
view release on metacpan or search on metacpan
lib/Langertha/Knarr/Protocol/A2A.pm view on Meta::CPAN
package Langertha::Knarr::Protocol::A2A;
# ABSTRACT: Google Agent2Agent (A2A) wire protocol for Knarr
our $VERSION = '1.100';
use Moose;
use JSON::MaybeXS;
use Data::UUID;
use Time::HiRes qw( time );
use Langertha::Knarr::Request;
use Langertha::Knarr::Response;
with 'Langertha::Knarr::Protocol';
# --- A2A overview ---
# Discovery: GET /.well-known/agent.json -> AgentCard JSON
# Method bus: POST / body: JSON-RPC 2.0 envelope
# methods: "tasks/send", "tasks/sendSubscribe", "tasks/get",
# "tasks/cancel", "tasks/pushNotification/set", ...
#
# tasks/send (sync): returns Task JSON in JSON-RPC result
# tasks/sendSubscribe (streaming): returns SSE stream of JSON-RPC responses
# Each event is a full JSON-RPC response wrapping a TaskStatusUpdateEvent or
# TaskArtifactUpdateEvent. Final event has status.state = "completed".
#
# Streaming model on the wire:
# data: {"jsonrpc":"2.0","id":<reqid>,"result":{"id":"<task>","status":{"state":"working","message":{...}}, "final":false}}\n\n
# data: {"jsonrpc":"2.0","id":<reqid>,"result":{"id":"<task>","artifact":{"parts":[{"type":"text","text":"Hi"}], "index":0,"append":true}, "final":false}}\n\n
# data: {"jsonrpc":"2.0","id":<reqid>,"result":{"id":"<task>","status":{"state":"completed"},"final":true}}\n\n
# ---------------------------------------------------------------------------
has _json => ( is => 'ro', default => sub { JSON::MaybeXS->new( utf8 => 1, canonical => 1 ) } );
has _uuid => ( is => 'ro', default => sub { Data::UUID->new } );
has agent_card => (
is => 'ro',
isa => 'HashRef',
lazy => 1,
builder => '_build_agent_card',
);
sub _build_agent_card {
my ($self) = @_;
return {
name => 'Langertha Steerboard Agent',
description => 'Steerboard-exposed agent',
url => '/',
version => '0.0.1',
capabilities => {
streaming => JSON::MaybeXS::true(),
pushNotifications => JSON::MaybeXS::false(),
stateTransitionHistory => JSON::MaybeXS::false(),
},
defaultInputModes => ['text'],
defaultOutputModes => ['text'],
skills => [],
};
}
sub protocol_name { 'a2a' }
sub protocol_routes {
return [
{ method => 'GET', path => '/.well-known/agent.json', action => 'a2a_card' },
{ method => 'POST', path => '/', action => 'chat' },
];
}
# A2A puts everything through JSON-RPC; we surface a normalized Steerboard
# request from the "tasks/send" or "tasks/sendSubscribe" method params.
sub parse_chat_request {
my ($self, $http_req, $body_ref) = @_;
my $env = $self->_json->decode( $$body_ref || '{}' );
my $method = $env->{method} // '';
my $params = $env->{params} // {};
my $stream = $method eq 'tasks/sendSubscribe' ? 1 : 0;
my $msg = $params->{message} // {};
my @parts = @{ $msg->{parts} || [] };
my $text = join '', map { ($_->{type} // '') eq 'text' ? ($_->{text} // '') : '' } @parts;
return Langertha::Knarr::Request->new(
protocol => 'a2a',
raw => $env,
messages => [ { role => 'user', content => $text } ],
stream => $stream,
session_id => $params->{sessionId} // $params->{id},
extra => {
jsonrpc_id => $env->{id},
task_id => $params->{id} // $self->_uuid->create_str,
a2a_method => $method,
},
);
}
sub format_chat_response {
my ($self, $response, $request) = @_;
my $content = Langertha::Knarr::Response->coerce($response)->content;
my $task = {
id => $request->extra->{task_id},
sessionId => $request->session_id,
status => {
state => 'completed',
message => {
role => 'agent',
parts => [ { type => 'text', text => $content } ],
},
},
artifacts => [
{ parts => [ { type => 'text', text => $content } ], index => 0, append => JSON::MaybeXS::false() },
lib/Langertha/Knarr/Protocol/A2A.pm view on Meta::CPAN
status => { state => 'working' },
final => JSON::MaybeXS::false(),
});
}
sub format_stream_chunk {
my ($self, $delta_text, $request) = @_;
return $self->_rpc_event( $request, {
id => $request->extra->{task_id},
artifact => {
parts => [ { type => 'text', text => $delta_text } ],
index => 0,
append => JSON::MaybeXS::true(),
},
final => JSON::MaybeXS::false(),
});
}
sub format_stream_close {
my ($self, $request) = @_;
return $self->_rpc_event( $request, {
id => $request->extra->{task_id},
status => { state => 'completed' },
final => JSON::MaybeXS::true(),
});
}
sub format_stream_done { '' }
__PACKAGE__->meta->make_immutable;
1;
__END__
=pod
=encoding UTF-8
=head1 NAME
Langertha::Knarr::Protocol::A2A - Google Agent2Agent (A2A) wire protocol for Knarr
=head1 VERSION
version 1.100
=head1 DESCRIPTION
Implements the Google A2A protocol on top of
L<Langertha::Knarr::Protocol>. Loaded by default.
=over
=item * C<GET /.well-known/agent.json> â agent card discovery (anonymous)
=item * C<POST /> â JSON-RPC 2.0 method bus
=back
Supported methods: C<tasks/send> (sync) and C<tasks/sendSubscribe>
(streaming). Streaming wraps every chunk in a JSON-RPC envelope with
the original C<id> preserved, transitions C<status.state> from
C<working> to C<completed>, and emits artifact append events for the
text deltas.
=head2 agent_card
The HashRef returned by the discovery endpoint. Defaults to a generic
"Knarr Agent" card with C<streaming: true>; override to advertise
specific skills, version, etc.
=head1 SUPPORT
=head2 Issues
Please report bugs and feature requests on GitHub at
L<https://github.com/Getty/langertha-knarr/issues>.
=head2 IRC
Join C<#langertha> on C<irc.perl.org> or message Getty directly.
=head1 CONTRIBUTING
Contributions are welcome! Please fork the repository and submit a pull request.
=head1 AUTHOR
Torsten Raudssus <torsten@raudssus.de> L<https://raudssus.de/>
=head1 COPYRIGHT AND LICENSE
This software is copyright (c) 2026 by Torsten Raudssus.
This is free software; you can redistribute it and/or modify it under
the same terms as the Perl 5 programming language system itself.
=cut
( run in 0.454 second using v1.01-cache-2.11-cpan-600a1bdf6e4 )