Sr. Content Developer at Microsoft, working remotely in PA, TechBash conference organizer, former Microsoft MVP, Husband, Dad and Geek.
164267 stories
·
33 followers

Securing an Angular Firebase AI App with Email/Password Authentication (Part 2: Signal Forms Sign-In Modal) [GDE]

1 Share

In this blog post, we build a Sign-In modal component (SignInModalComponent) using the stable Angular Signal Forms API (@angular/forms/signals) to handle email and password authentication.

Sign-In Dialog with email and password fields
Caption: Sign-In Dialog with email and password fields.

Architecture & Mental Model

The sign-in flow works as follows:

  • Signal Form Validation: We build a Signal Form with required email and password fields. When validation fails on submit, onInvalid automatically focuses the first invalid control.
  • Firebase Authentication: When a user submits valid form fields, SignInModalComponent calls AuthService.signIn() to verify that the submitted email and password match an existing account in Firebase Authentication.
  • Route Redirection or Error Handling:
    • Valid credentials: When authentication succeeds (AuthService.isAuthenticated() is true), the modal closes and redirects the user to the /dashboard route, where they can perform AI tasks.
    • Invalid credentials / error thrown: When authentication fails, SignInModalComponent.signIn() updates its errorMessage signal ('Failed to sign in. Please check your credentials and try again.') and returns { kind: 'serverError', message: 'Failed to submit form' }.
flowchart TD
    Submit["User Submits Email & Password"] --> Validate{"signInSchemaValidation()"}
    Validate -- "Invalid" --> Focus["onInvalid: focusBoundControl() on 1st error"]
    Validate -- "Valid" --> Action["submission.action: signIn(field)"]
    Action --> Auth["AuthService.signIn(formValues)"]
    Auth --> Check{"AuthService.isAuthenticated()?"}
    Check -- "true" --> Close["DialogRef.close() + Router.navigate(['/dashboard'])"]
    Check -- "false / throws" --> Error["Set errorMessage signal & return serverError"]

1. Defining the Signal Form Validation Schema

We start by importing the required and email validators from @angular/forms/signals to define signInSchemaValidation(), which returns a SchemaOrSchemaFn<AuthCredentials, PathKind.Root>:

  • Required fields (required): The required validator marks both the email and password fields as required with custom error messages ({ message: 'Email is required' } and { message: 'Password is required' }).
  • Email format validation (email): The email validator checks that the entered email value is a well-formed email address and returns { message: 'Invalid email format' } when the format is invalid.
// src/app/shared/ui/sign-in-modal/schemas/sign-in.schema.ts
import type { AuthCredentials } from '@/core/auth';
import type { PathKind, SchemaOrSchemaFn } from '@angular/forms/signals';
import { email, required } from '@angular/forms/signals';

export function signInSchemaValidation(): SchemaOrSchemaFn<AuthCredentials, PathKind.Root> {
  return (schemaPath) => {
    required(schemaPath.email, { message: 'Email is required' });
    email(schemaPath.email, { message: 'Invalid email format' });
    required(schemaPath.password, { message: 'Password is required' });
  };
}

2. Building SignInModalComponent with Signal Forms and AuthService

2.1 Injecting Dependencies and Initializing the Signal Form

In SignInModalComponent, we inject the required services and initialize the reactive form model:

  • AuthService and Router: Handle Firebase email/password authentication and route navigation after signing in.
  • DialogRef: Closes the modal via @angular/cdk/dialog (inject(DialogRef, { optional: true })) when the user clicks the close button or when sign-in succeeds.
  • errorMessage: Stores and displays an alert message (signal<string | undefined>(undefined)) when authentication fails.
  • signInModel: Backs the Signal Form (signal<AuthCredentials>({ email: '', password: '' })) and initializes the email and password fields to empty strings.

Note

Why @angular/cdk/dialog instead of a native <dialog> element?
If you only need a single modal in an app without @angular/cdk, the native HTML <dialog> element is the better, zero-dependency choice. However, this repository already includes @angular/cdk, and @angular/cdk/dialog provides comprehensive accessibility (a11y) out of the box—including focus trapping, focus restoration, Escape-key dismissal, and ARIA attributes. Because we also dynamically import @angular/cdk/dialog when opening the modal, so it never increases the initial bundle size, it is a practical choice here.

// src/app/core/auth/auth-credentials.interface.ts
export interface AuthCredentials {
  email: string;
  password: string;
}
// src/app/shared/ui/sign-in-modal/sign-in-modal.component.ts (Part 1a: Dependencies & Model)
@Component({
  imports: [FormField, FormRoot, CloseIconComponent, SpinnerIconComponent],
  selector: 'app-sign-in-modal',
})
export class SignInModalComponent {
  readonly #authService = inject(AuthService);
  readonly #dialogRef = inject(DialogRef, { optional: true });
  readonly #router = inject(Router);

  errorMessage = signal<string | undefined>(undefined);

  signInModel = signal<AuthCredentials>({
    email: '',
    password: '',
  });

Next, we pass this.signInModel and signInSchemaValidation() to the form() function to create signInForm. The form() function validates signInModel against the validation schema and configures form submission behavior:

  • Valid submission (submission.action): When validation succeeds, form submission executes the action callback, which invokes this.signIn(field).
  • Invalid submission (submission.onInvalid): When validation fails, form submission executes the onInvalid callback to locate the first validation error (field().errorSummary()[0]) and automatically focus its bound form control (firstError?.fieldTree().focusBoundControl()).
// src/app/shared/ui/sign-in-modal/sign-in-modal.component.ts (Part 1b: Signal Form)
  signInForm = form(this.signInModel, signInSchemaValidation(), {
    submission: {
      action: async (field) => this.signIn(field),
      onInvalid: (field) => {
        const firstError = field().errorSummary()[0];
        firstError?.fieldTree().focusBoundControl();
      },
    },
  });

2.2 Implementing onClose() and signIn()

Next, let's implement the onClose() and signIn() methods in SignInModalComponent:

  • onClose(): Calls this.#dialogRef?.close() when the user clicks the top-right close button to dismiss the modal.
  • signIn(): Calls this.#authService.signIn(field().value()) to authenticate the submitted form credentials with Firebase Authentication:
    • Successful authentication: When this.#authService.isAuthenticated() evaluates to true, we close the modal (this.#dialogRef?.close()), navigate to the /dashboard route (await this.#router.navigate([APP_LINKS.DASHBOARD])), and return undefined.
    • Unsuccessful authentication: When authentication fails, we update this.errorMessage.set('Failed to sign in. Please check your credentials and try again.') and return a generic submission error ({ kind: 'serverError', message: 'Failed to submit form' }) so the Signal Form records the failed submission state.
// src/app/shared/ui/sign-in-modal/sign-in-modal.component.ts (Part 2: onClose & signIn)
  onClose(): void {
    this.#dialogRef?.close();
  }

  async signIn(field: FieldTree<AuthCredentials, string | number>): Promise<SignInSubmissionError | undefined> {
    this.errorMessage.set(undefined);
    await this.#authService.signIn(field().value());
    if (this.#authService.isAuthenticated()) {
      this.#dialogRef?.close();
      await this.#router.navigate([APP_LINKS.DASHBOARD]);
      return undefined;
    }
    this.errorMessage.set('Failed to sign in. Please check your credentials and try again.');
    return { kind: 'serverError', message: 'Failed to submit form' };
  }
}

3. Binding the Signal Form in the Template

Now let's wire signInForm to the component template in src/app/shared/ui/sign-in-modal/sign-in-modal.component.html:

  • Closing the modal: When a user clicks the top-right close button, onClose() closes the modal via this.#dialogRef?.close().
  • Displaying authentication errors: When errorMessage() holds a message (@if (errorMessage(); as msg)), the template renders <div role="alert">{{ msg }}</div> above the form.
  • Binding the form root ([formRoot]): The [formRoot] directive binds the <form> element directly to the signInForm form model, connecting form submission to the submission configuration we defined in the component.
  • Binding form fields ([formField]): The [formField] directive binds signInForm.email to the email input field and signInForm.password to the password input field.
  • Rendering validation errors: When the user interacts with a field (touched()) and it is invalid(), we iterate over its errors() array with @for to output each validation error message ({{ error.message }}).
  • Handling the submission state: While the form's async submission.action is running, signInForm().submitting() evaluates to true. We bind [disabled]="signInForm().submitting()" on the submit button to disable it during authentication and prevent concurrent submissions.
<!-- src/app/shared/ui/sign-in-modal/sign-in-modal.component.html (Signal Form excerpt) -->
@if (errorMessage(); as msg) {
  <div role="alert">{{ msg }}</div>
}

<form [formRoot]="signInForm">
  <label for="sign-in-email">
    EMAIL ADDRESS
    <input id="sign-in-email" type="email" autocomplete="username" [formField]="signInForm.email" />
    @if (signInForm.email().touched() && signInForm.email().invalid()) {
      @for (error of signInForm.email().errors(); track error) {
        <span>{{ error.message }}</span>
      }
    }
  </label>

  <label for="sign-in-password">
    PASSWORD
    <input id="sign-in-password" type="password" autocomplete="current-password" [formField]="signInForm.password" />
    @if (signInForm.password().touched() && signInForm.password().invalid()) {
      @for (error of signInForm.password().errors(); track error) {
        <span>{{ error.message }}</span>
      }
    }
  </label>

  <button type="submit" [disabled]="signInForm().submitting()">
    @if (signInForm().submitting()) {
      <app-spinner-icon />
      <span>Signing in...</span>
    } @else {
      <span>Sign In</span>
    }
  </button>
</form>

Sign-In Dialog with an invalid authentication attempt
Caption: Sign-In Dialog with an invalid authentication attempt.

Sign-In Dialog when the form fields violate the required field validation rules
Caption: Sign-In Dialog when the form fields violate the required field validation rules.

Takeaways & What's Next

In Part 2, we built SignInModalComponent and connected it to AuthService:

  • Angular Signal Forms Integration: SignInModalComponent uses Angular Signal Forms to define the form model and bind each field to its corresponding HTML <input> element. signInSchemaValidation() validates the form fields. When validation passes, submission.action calls AuthService.signIn() to authenticate the user's credentials; when validation fails, submission.onInvalid automatically focuses the first field with an error.
  • Lean Initial Bundles with Dynamic Imports: Even though @angular/cdk is not a lightweight library, we will dynamically import both SignInModalComponent and @angular/cdk/dialog when opening the modal from the Home component. This keeps main.js and our initial bundles lean.

In Part 3, we will build a lightweight Home screen with a Sign In button that opens the CDK Dialog. We will also update the Header component to provide icons for navigating home and logging out. Finally, we will use the AuthService.isAuthenticated() computed signal to conditionally display the Sign In and Launch Studio buttons on the Home screen, and reuse the same signal to show or hide the logout button in the Header component.

Resources

Read the whole story
alvinashcraft
just a second ago
reply
Pennsylvania, USA
Share this story
Delete

The A2A CLI and Rust: The Same Code, Three Ways to Serve It

1 Share

This article provides a step by step guide to the A2A CLI, driven against Rust agents that speak A2A Protocol v1.0. The same Rust code, a Lucas-Lehmer test over the first N Mersenne prime exponents, is served three ways: on the official A2A Rust SDK, as a plain stdin/stdout program wrapped by a2a server --exec, and behind an ADK coordinator on Gemini started with the ADK CLI. Each one is timed from the A2A CLI and from a Rust client.

https://github.com/xbill9/rust-dev/tree/main/a2a-benchmark-rust

The A2A CLI talks to all three with the same commands, and a2a server --exec turns a 30-line program with no A2A code into an agent that ADK can call. One a2a send from a shell takes a median 20.74 ms against the SDK agent, of which 7.62 ms is starting the CLI. From a Rust client the SDK agent adds 0.55 to 1.40 ms per call, the wrapped program 3.33 to 5.05 ms, and the ADK coordinator a median 1328 ms on gemini-3.5-flash and 2283 ms on gemini-3.8-flash. Every call through N=26 returned the right count of primes.

What Is Being Built?

Agent Port How it speaks A2A
mersenne-agent 8104 Official Rust SDK, JSON-RPC at /jsonrpc and HTTP+JSON at /rest
mersenne-exec 8105 A stdin/stdout Rust program served by a2a server --exec
rust_bench 8106 ADK LlmAgent on Gemini, served by adk api_server --a2a
echo 8108 a2a server --echo, for the first command
a2a CLI ──► adk api_server --a2a (:8106) ──Gemini──► RemoteA2aAgent ──► mersenne-agent (:8104)
                    rust_bench                                     └──► a2a server --exec (:8105) ──► mersenne-exec
                        └── run_benchmark tool ──► mersenne-bench ──► both Rust agents

Versions, read from the machine on 2026-11-03:

Component Version
A2A protocol 1.0
A2A CLI (a2a) v0.3.1
a2a-lf / a2a-server-lf / a2a-client-lf 0.4.2 / 0.5.3 / 0.2.9
Rust 1.99.1, edition 2024
google-adk / a2a-sdk / google-genai 2.13.0 / 1.3.0 / 2.34.0
Model gemini-3.8-flash on Vertex AI

The a2a-*-lf crates are the official SDK from a2aproject/a2a-rs. Agent cards carry the protocol as Major.Minor, so they read 1.0.

Step 1 — Install the A2A CLI

The CLI is one Go binary. Homebrew, WinGet and release binaries work; from source:

go install github.com/a2aproject/a2a-cli@latest
mv "$(command -v a2a-cli)" "$(dirname "$(command -v a2a-cli)")/a2a"
a2a version
a2a v0.3.1
commit:  none
built:   unknown
go:      go1.26.3
os/arch: linux/amd64

go install names the binary a2a-cli; the docs and the CLI's own skill call it a2a.

Step 2 — Talk to an Echo Server

a2a server --echo starts an agent that sends back what it receives, so the client can be checked before any agent code exists:

a2a server --echo --port 8108 &
a2a send -a http://localhost:8108 "hello world from A2A"
Task:     01a1a35f-2b2b-7f3a-92a2-e3dc6c69e359
Context:  01a1a35f-2b2c-7120-ac45-a74adab9d1d2
Status:   completed (2026-11-03T16:44:45-05:00)
Artifacts:
  [01a1a35f-2b2c-71ba-8e6c-22c4b7c210df] hello world from A2A
History:
  [user] hello world from A2A

Every send creates a task with an id, a context, a final status and its artifacts. The Go servers started by the CLI stamp local time; the Rust SDK agent stamps UTC.

Step 3 — Build and Start the Rust Agents

The rig is one Cargo project with four binaries: the SDK agent, the stdin/stdout program, the benchmark client, and a shared library holding the Lucas-Lehmer code.

git clone https://github.com/xbill9/rust-dev
cd rust-dev/a2a-benchmark-rust
cargo build --release
scripts/start-all.sh
agents up: 8104 native, 8105 exec

start-all.sh rebuilds, stops any running copy, starts the SDK agent on 8104 and a2a server --exec on 8105 with logs in logs/, and waits until both cards answer. scripts/test-cli.sh runs the CLI commands below against them and checks each reply.

Step 4 — Read an Agent Card

card get fetches /.well-known/agent-card.json and prints what the agent offers:

a2a card get -a http://localhost:8104
Name:         Mersenne Prime Agent Rust
Description:  A Rust A2A v1.0 agent (official a2a-rs SDK) that confirms the first n Mersenne primes with the Lucas-Lehmer test and reports the elapsed time.
Version:      0.1.0
Interfaces:
  JSONRPC (A2A v1.0)     http://localhost:8104/jsonrpc
  HTTP+JSON (A2A v1.0)   http://localhost:8104/rest
Streaming:    true
Skills:
  find-mersenne-rust   Find Mersenne Primes in Rust

The card lists two interfaces. The CLI picks one, and --transport chooses.

Step 5 — Send, Stream, Switch Transport, Send Data

A plain message:

a2a send -a http://localhost:8104 "Calculate the first 12 Mersenne primes"
Status:   completed (2026-11-03T21:44:45Z)
  Found first 12 Mersenne primes in 0.0902ms.
Artifacts:
  [mersenne-report] Found first 12 Mersenne primes in 0.0902ms. {"elapsed_ms":0.090239,"exponents":[2,3,5,7,13,17,19,31,61,89,107,127],"found":12,"requested":12}

The mersenne-report artifact carries two parts: the summary as text, and the same result as a data part, so a client reads the timing from a field.

--stream prints each event as it arrives, here the working status before the result:

a2a send -a http://localhost:8104 --stream "first 8"
[status] working
Task:     01a1a35f-2b64-77b5-9417-18b1f425b17d
Context:  01a1a35f-2b64-77b5-9417-18b24253b719
Status:   completed (2026-11-03T21:44:45Z)
  Found first 8 Mersenne primes in 0.0164ms.

--transport rest sends the same request over HTTP+JSON, and --data-part sends structured input:

a2a send -a http://localhost:8104 --transport rest "first 4"
a2a send -a http://localhost:8104 --data-part '{"count": 6}'
  Found first 4 Mersenne primes in 0.0080ms.
  Found first 6 Mersenne primes in 0.0108ms.

🔎 Tip: Data Parts Arrive as Floats

A2A v1.0 carries data parts as a protobuf Struct, whose only number type is a double. {"count": 6} sent by the CLI reaches the agent as 6.0, so a strict integer field fails to deserialise. Read numbers with as_f64 and check for a whole value:

if let PartContent::Data(v) = &part.content
    && let Some(n) = v.get("count").and_then(|n| n.as_f64())
    && n >= 1.0
    && n.fract() == 0.0
{
    return n as usize;
}

Step 6 — Script It With JSON Output

-o json prints the whole task, which pipes into anything:

a2a send -a http://localhost:8104 -o json 'first 3' | python3 -c 'import sys,json; t=json.load(sys.stdin)["task"]; print(t["status"]["state"], [p for a in t["artifacts"] for p in a["parts"] if "data" in p][0]["data"])'
TASK_STATE_COMPLETED {'elapsed_ms': 0.007166, 'exponents': [2, 3, 5], 'found': 3, 'requested': 3}

Step 7 — Tasks: List, Start in the Background, Wait

The SDK agent keeps its tasks in memory, and the CLI reads them back:

a2a task list -a http://localhost:8104
ID                                    STATUS     CONTEXT
01a1a35f-2b4f-7190-b6df-81bb7b76c2cc  completed  01a1a35f-2b4f-7190-b6df-81bca35e8b6d
01a1a35f-2b64-77b5-9417-18b1f425b17d  completed  01a1a35f-2b64-77b5-9417-18b24253b719
01a1a35f-2bab-7012-b094-1022dce75f36  completed  01a1a35f-2bab-7012-b094-1023824b35eb

--async returns as soon as the task exists. N=25 takes over a second, long enough to catch it running:

a2a send -a http://localhost:8104 --async "first 25"
Task:     01a1a3f5-9da1-746b-abae-84307e4e1735
Context:  01a1a3f5-9da1-746b-abae-8431b422e72e
Status:   working (2026-11-04T00:29:05Z)
History:
  [user] first 25

task get --wait polls until the task finishes:

a2a task get -a http://localhost:8104 01a1a3f5-9da1-746b-abae-84307e4e1735 --wait --poll-interval 500ms
Task:     01a1a3f5-9da1-746b-abae-84307e4e1735
Context:  01a1a3f5-9da1-746b-abae-8431b422e72e
Status:   completed (2026-11-04T00:29:06Z)
  Found first 25 Mersenne primes in 1509.7683ms.

Step 8 — The Native Rust Agent

The SDK agent is one AgentExecutor. It emits a working status, runs the big-integer work on a blocking thread, then returns the finished task:

let done = async move {
    // Big-integer work is CPU-bound; keep it off the async workers.
    let report = tokio::task::spawn_blocking(move || find_mersenne_primes(count))
        .await
        .map_err(|e| A2AError::internal(format!("worker panicked: {e}")))?;
    let summary = report.summary();
    let data = serde_json::to_value(&report)
        .map_err(|e| A2AError::internal(format!("serialise report: {e}")))?;
    // ... Task { status: Completed, artifacts: [Part::text(summary), Part::data(data)] }
};
Box::pin(stream::once(async { Ok(working) }).chain(stream::once(done)))

The server wires the SDK's routers into axum, one per transport plus the card:

let handler = Arc::new(DefaultRequestHandler::new(MersenneExecutor, InMemoryTaskStore::new()));
let card = Arc::new(StaticAgentCard::new(agent_card(&base)));

let app = axum::Router::new()
    .nest("/jsonrpc", a2a_server::jsonrpc::jsonrpc_router(handler.clone()))
    .nest("/rest", a2a_server::rest::rest_router(handler))
    .merge(a2a_server::agent_card::agent_card_router(card));

The Lucas-Lehmer test runs on num-bigint with the reduction mod 2^p − 1 done by folding the high bits onto the low ones. The timed region holds only the tests, on a monotonic clock, and no prime is ever turned into a decimal string. Two slow tests check the arithmetic: one runs the test on every prime exponent up to 9941 and asserts it accepts exactly the first 22 known ones, the other confirms all 26:

cargo test --release --lib -- --ignored
test tests::all_26_exponents_prime ... ok
test tests::exhaustive_through_n22 ... ok

test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 5 filtered out; finished in 25.70s

Step 9 — Serve a Plain Program With a2a server --exec

mersenne-exec knows nothing about A2A. It reads the message on stdin and prints two lines:

echo 'first 5' | target/release/mersenne-exec
{"requested":5,"found":5,"elapsed_ms":0.007023000000000001,"exponents":[2,3,5,7,13]}
Found first 5 Mersenne primes in 0.0070ms.

The A2A CLI makes it an agent:

a2a server --exec "$PWD/target/release/mersenne-exec" --chunk=$'\n' \
  --name "Mersenne Prime Exec Rust" \
  --advertise-address "http://localhost:8105" --port 8105

The same client commands work against it:

a2a send -a http://localhost:8105 --stream "first 8"
Status:   submitted
History:
  [user] first 8
[status] working
[artifact] {"requested":8,"found":8,"elapsed_ms":0.01543,"exponents":[2,3,5,7,13,17,19,31]}Found first 8 Mersenne primes in 0.0154ms.
[status] completed

The exec server starts the program once per message, hands it the text, and returns stdout as the artifact.

🔎 Tip: Two --exec Flags ADK Needs

  • --advertise-address http://localhost:8105. The exec server's card names 127.0.0.1 by default, and ADK's RemoteA2aAgent refuses a card whose RPC URL has a different origin from the one the card came from.
  • --chunk=$'\n'. With it, each output line streams as its own chunk. Without it, stream requests get UNSUPPORTED_OPERATION: the Go CLI falls back to polling and ADK's Python client fails. ADK keeps only the last chunk, so mersenne-exec prints the JSON line first and the summary last.

Step 10 — Put ADK in Front

ADK needs its Python package and Gemini on Vertex AI, signed in with application-default credentials:

pip install -U "google-adk[a2a]"
gcloud auth application-default login
source scripts/adk-env.sh

adk-env.sh sets GOOGLE_GENAI_USE_VERTEXAI=TRUE, GOOGLE_CLOUD_LOCATION=global, the project, and MODEL_NAME, which defaults to gemini-3.8-flash.

adk/rust_bench/agent.py is an ADK LlmAgent with both Rust agents as RemoteA2aAgent sub-agents and one tool:

native_agent = RemoteA2aAgent(
    name="native_rust_agent",
    description="Rust A2A v1.0 agent on the official a2a-rs SDK. ...",
    agent_card=f"{NATIVE_URL}{AGENT_CARD_WELL_KNOWN_PATH}",
)

root_agent = LlmAgent(
    name="rust_bench",
    model=os.getenv("MODEL_NAME", "gemini-3.8-flash"),
    tools=[run_benchmark],
    sub_agents=[native_agent, exec_agent],
)

The ADK CLI runs it three ways:

adk run rust_bench                                # terminal
adk web --port 8107                               # dev UI
adk api_server --a2a --port 8106 adk              # A2A server, from the rig root

adk api_server --a2a serves each agent folder that holds an agent.json card, at /a2a/<name>; scripts/start-adk.sh starts it in the background. The A2A CLI then treats the coordinator like any other agent:

a2a send -a http://localhost:8106/a2a/rust_bench/.well-known/agent-card.json \
  --timeout 120s "Find the first 9 Mersenne primes with the exec agent"
Status:   completed (2026-11-03T21:44:51Z)
Artifacts:
  [f2d2f27d-24ce-4120-8368-d966c99617a9] Found first 9 Mersenne primes in 0.0236ms.
History:
  [user] Find the first 9 Mersenne primes with the exec agent
  [agent] {"args":{"agent_name":"exec_rust_agent"},"id":"call_1332004","name":"transfer_to_agent"}
  [agent] {"id":"call_1332004","name":"transfer_to_agent","response":{"result":null}}
  [agent] {"requested":9,"found":9,"elapsed_ms":0.023635,"exponents":[2,3,5,7,13,17,19,31,61]}
  [agent] Found first 9 Mersenne primes in 0.0236ms.

The history shows the hand-off: Gemini calls transfer_to_agent, the Rust agent answers, and its JSON report comes back through two A2A hops.

Step 11 — Keep the Model Away From the Numbers

A benchmark through an LLM can time the model writing a number. Two rules keep the timing in code:

  • The benchmark client checks every reply. mersenne-bench reads the timing from the agent's data part or JSON line, rejects a report where requested differs from N or found differs from min(N, 26), and rejects an LLM-path reply with no transfer_to_agent to a Rust agent. Each row records where its report came from and who it was routed to.
  • ADK benchmarks go through a tool. run_benchmark runs mersenne-bench and returns medians already computed and rounded. The instruction tells Gemini to quote them, and scripts/check-adk-reply.py verifies a reply: one run_benchmark call, with the arguments asked for, and every number in the answer a value the tool returned.

A benchmark request sent from the CLI, saved with -o json. The tool call and the head of the answer:

a2a send -a $CARD --timeout 300s -o json "Benchmark both agents for N=3 to 6 with 2 reps" > reply.json
[tool call] {"max_n": 6, "min_n": 3, "reps": 2}
### Arguments Used
- `min_n`: 3
- `max_n`: 6
- `reps`: 2

The answer goes on with a summary per agent and a row per N. The check:

scripts/check-adk-reply.py reply.json 3 6 2
state: TASK_STATE_COMPLETED
run_benchmark calls: 1
args sent: {'max_n': 6, 'min_n': 3, 'reps': 2}  requested: {'min_n': 3, 'max_n': 6, 'reps': 2}
numbers in reply: 57  not from the tool: 0
PASS

scripts/adk-reliability.sh sends ten benchmark requests with varied ranges and ten requests that name no agent. On gemini-3.8-flash, 10 of 10 benchmark replies passed the checker and 10 of 10 unnamed requests went to native_rust_agent.

The sweep covers the whole exponent table, N=1 to 26. Exponent 23209 is a 6,987-digit number, and the arithmetic stays in BigUint throughout.

Step 12 — Benchmark From a Rust Client

REPS=20 scripts/bench.sh          # native + exec, N=1..26
ADK=1 REPS=3 scripts/bench.sh     # adds the ADK series

mersenne-bench is built on the SDK's client crate, a2a-client-lf. Each call records the agent's own compute time and the client's round trip; overhead is the round trip minus compute, per call. Direct calls, medians of 20 reps:

N native compute ms native overhead ms exec compute ms exec overhead ms
1 0.001039 0.731 0.0004555 4.046
10 0.04249 0.793 0.04200 4.391
20 26.33 0.970 60.21 4.374
22 181.1 1.255 264.6 3.558
24 781.1 1.073 868.9 3.335
26 2484 1.396 2619 3.588

Through ADK, medians of 3 reps, one run per model. All 26 rows in each run went to native_rust_agent and read its data part:

N 3.5-flash compute ms 3.5-flash round trip ms 3.8-flash compute ms 3.8-flash round trip ms
1 0.002051 1665.053 0.002615 2073.666
10 0.04513 1394.245 0.04390 2458.155
20 72.95 1332.994 68.52 2336.569
22 279.8 1868.178 287.3 2679.324
26 2226 3455.492 2241 4123.353

The slowest single call on gemini-3.8-flash took 29.0 s, at N=9.

Step 13 — Time the A2A CLI Itself

scripts/cli-bench.py runs one a2a send -o json process per call, the way a shell script would, and applies the same checks to each reply. Wall time covers starting the CLI, fetching the card, sending, and exiting. Two reference commands, 20 runs each:

Command Median ms Min ms Max ms
a2a version 7.17 5.57 9.62
a2a card get (native) 9.18 8.30 11.17

The full sweep, N=1 to 26, 5 reps direct and 3 through ADK:

Agent Calls Overhead per call, per-N medians Median of the per-N medians
native 130 10.92 to 21.82 ms 20.28 ms
exec 130 9.43 to 20.81 ms 17.51 ms
ADK, 3.8-flash 78 1825.22 to 3017.92 ms 2217.55 ms

Every ADK call went to native_rust_agent; the slowest took 22693.6 ms. The sweep times each agent in its own block, so native and exec ran minutes apart. Run in alternating order, N=1, 30 runs each (scripts/cli-transport.py):

native, default (JSON-RPC): median 20.74 ms, min 18.60, max 26.51
native, --transport rest: median 21.49 ms, min 19.63, max 26.15
exec (HTTP+JSON only): median 20.81 ms, min 18.67, max 28.07
a2a version: median 7.62 ms, min 6.07, max 10.71

From a shell, the transport and the serving path make no difference to the CLI's cost: about 21 ms per call, about a third of it starting the process. Against the Rust client's 0.55 to 1.40 ms, the CLI fits scripts and checks, and the Rust client fits a benchmark.

The CLI's overhead in the sweep drops to about 11 ms for both agents once compute passes 100 ms (N=21 and up). Why is untested.

🔎 Tip: Compare Compute Only Between Runs Paced Alike

The same native process at N=20 takes 26.07 ms when calls arrive back to back and 68.09 ms when each call follows a 1.3 s idle gap (mersenne-bench --gap-ms 1300). ADK's model round trip spaces calls out too, and its mid-range compute times sit close to the gap series: 68.52 ms at N=20 on gemini-3.8-flash. The CPU-level cause, clock scaling or core placement on a hybrid laptop CPU, is untested.

Compare and Contrast

Serving path Code the agent needs Overhead from a Rust client a2a send from a shell, N=1
Official Rust SDK 🥇 an AgentExecutor and three routers 0.55 to 1.40 ms 20.74 ms
a2a server --exec 🥈 none: stdin in, stdout out 3.33 to 5.05 ms 20.81 ms
ADK coordinator 🥉 an agent.py, an agent.json card 1328 ms on 3.5-flash, 2283 ms on 3.8-flash 2664.00 ms on 3.8-flash

Rust-client ranges are the smallest and largest per-N medians; the ADK figures are the medians of its per-N medians. The exec figure includes starting a process for every message; how it splits between process start and the HTTP hop was not measured. At N=26 the work itself dominates every path: 2484 ms native and 2619 ms exec, direct.

So, Which One?

For an agent built to be one, use the SDK: about a millisecond of protocol cost, both transports, tasks the CLI can list and wait on.

For a program that already exists, a2a server --exec makes it an agent with no code change. A few milliseconds per call buys that, and ADK can call it once the two flags are set.

For routing by natural language, put ADK in front and keep the timing out of the model: a tool for anything computed, and a check that the model sent the arguments asked for.

For talking to any of them, the A2A CLI is the one client: card get, send, --stream, task get --wait, and -o json for scripts. Each call costs about 21 ms, so time agents from a long-lived client.

Summary

The goal of this article was to drive the A2A CLI against Rust agents on A2A v1.0, and to measure what each way of serving the same Rust code costs per call. The key to the solution was one benchmark client that reads timing from a structured report and checks that every reply answers the question asked. The results on one laptop were:

  • 🟢 The A2A CLI drove all three agents with the same commands: card, send, stream, REST, data parts, JSON output, task list, --async and task get --wait
  • 🟢 Every call through N=26, from the Rust client and from the CLI, reported found = min(N, 26)
  • 🟢 The SDK agent added 0.55 to 1.40 ms per call from a Rust client
  • 🟢 a2a server --exec served an A2A-unaware program to the CLI and to ADK
  • 🟢 On gemini-3.8-flash, 10 of 10 benchmark replies quoted only tool values, and 10 of 10 unnamed requests went to the native agent
  • ⚠️ One a2a send from a shell costs about 21 ms whatever the agent, 7.62 ms of it starting the CLI
  • ⚠️ The wrapped program added 3.33 to 5.05 ms per call from a Rust client
  • ⚠️ ADK added a median 1328 ms per call on gemini-3.5-flash and 2283 ms on gemini-3.8-flash, with one call at 29.0 s
  • ❌ Compute times from calls spaced by idle gaps run slower and do not compare with back-to-back runs

Scope: one 13th Gen Intel Core i7-1360P laptop on Linux, release build, every agent and client on the same machine, 2026-11-03 and 2026-11-04. Rust-client series have 20 reps per N direct and 3 through ADK, with one run per model on Vertex AI's shared global endpoint, so the difference between the two Gemini models is a single sample. The CLI series has 5 reps per N direct and 3 through ADK, on gemini-3.8-flash only, and reliability was checked on that model only, ten requests of each kind. The gRPC transport, task cancel, task subscribe, push notifications and authentication were not exercised.

The strategy for serving and benchmarking Rust A2A agents with the A2A CLI was validated with an incremental step by step approach.

References

Read the whole story
alvinashcraft
just a second ago
reply
Pennsylvania, USA
Share this story
Delete

Jetpack 16.4 release: One Protect page, a new Backup dashboard, and more

1 Share

Jetpack 16.4 is out now. This Jetpack 16.4 release focuses on helping you do more without leaving your dashboard, starting with security and backups. Jetpack 16.4 was released on November 3, 2026, here’s what’s new.

All of your security on one page

Scan, Firewall, Login protection and Downtime monitoring now live together on one page, Jetpack → Protect. You can check whether your site is safe, fix threats, test your firewall and change any security settings there, without a trip to Jetpack Cloud or Settings → Security.

“Protect” replaces the “Scan ↗” link in the Jetpack menu. It’s on by default for sites running the Jetpack plugin, and WordPress.com sites get it soon.

You rely on Jetpack Protect already, now you’ll now find everything you need for security consolidated under Jetpack → Protect. We’ll share a closer look at the new page tomorrow.

A new Backup dashboard

The Backup dashboard in wp-admin has been rebuilt. Pick any backup to see what it holds and browse its files, then restore your site to that point or download it.

You can also choose when your daily backup runs and how many days of backups to keep, right from the dashboard. Backups in the Activity Log open straight to that backup, and backups that finished with warnings can now be opened and restored too. These Jetpack 16.4 features aim to reduce friction during restores and give you more control from wp-admin.

Trim and cut your videos

VideoPress has a new “Trim & cut” editor. Trim the start or end of a video, or cut a section out of the middle, then save over the original or keep it as a new video. You’ll find it in the VideoPress dashboard and in the Video block’s toolbar.

A comment form that fits your theme

If you use Jetpack Comments, your comment form has been rebuilt. It picks up your theme’s styling now, so the old “Color scheme” setting is gone. Commenters can format their text, add links, and drop in an image or an embed from sites WordPress trusts, like YouTube. Images only show once a comment is approved.

Simpler plans with Jetpack Pro

Starting today, there are two paid Jetpack plans to choose from, Pro and Complete. Jetpack Pro is $29 a month, billed yearly, and includes everything in Complete, from security and backups to VideoPress and AI, with lower limits on things like backup storage and Stats views.

Both plans renew at the price you paid, so there’s no first-year discount that doubles when it’s time to renew. Jetpack Security and Jetpack Growth are no longer sold to new customers. If you already have one of them, nothing changes and your plan keeps renewing as it does today. You can also switch from either one to Pro, with credit for the time left on your current plan. Take a look at both plans.

And more

The header in My Jetpack now shows whether your account is connected, with your plan and connection details a click away.

The old Modules page opens the “Features” tab in My Jetpack now. Features your site can’t use tell you why, and an “Available” filter hides them.

You can hand your site’s Jetpack connection over to another administrator without leaving your site, from the “Connectors” page.

The Newsletter dashboard has a new “Overview” tab, with a checklist that walks you through setting up your newsletter.

Account protection no longer turns on by itself for new connections. Sites that already use it keep it on.

Jetpack blocks that aren’t available now tell you why, like when a feature is turned off or Jetpack isn’t connected.

For everything else, check the full changelog.

Thanks to everyone who contributed to this release. You can find the full list of Jetpack contributors on GitHub.

Update to Jetpack 16.4 from your “Plugins” page, and let us know what you think in the support forums.





Read the whole story
alvinashcraft
29 seconds ago
reply
Pennsylvania, USA
Share this story
Delete

November 2026 Security Release

1 Share
The November 2026 security release for Next.js is now available
Read the whole story
alvinashcraft
37 seconds ago
reply
Pennsylvania, USA
Share this story
Delete

Filing: Amazon’s previously announced layoffs include 95 jobs in Washington state

1 Share
Amazon headquarters buildings in Seattle
Most of the employees in the latest Washington notice are based in Amazon’s Seattle offices. (GeekWire File Photo)

A notice made public Tuesday evening, disclosing about 95 Amazon job cuts in Washington state, is part of the company’s previously reported layoffs in its retail business, not a new round of reductions, a spokesperson confirmed.

The filing lists software engineers, product managers and program managers among the affected employees. Most are based in Amazon’s Seattle offices, but others work in Bellevue and Redmond, at warehouses in Sumner and Pasco, or remotely.

Reuters reported in early October that Amazon was laying off fewer than 1,000 corporate employees, mainly in its Stores unit, which runs the company’s e-commerce business. Amazon confirmed to GeekWire at the time that “hundreds” of Stores employees were affected. The notice covers only employees based in Washington state.

“We’ve adjusted parts of our Stores business because we believe this structure will better enable us to deliver on our priorities,” Amazon said in a statement at the time.

Most affected employees were notified in October, including 43 on or about Oct. 7, when the Stores cuts were first reported. Others received notice in September or as early as July 3, according to the filing.

The federal Worker Adjustment and Retraining Notification (WARN) Act requires large employers to give workers and the government advance notice of mass layoffs. Washington’s version of the WARN Act generally requires 60 days’ notice when a covered employer eliminates 50 or more jobs within 30 days across its Washington operations, excluding part-time employees.

Amazon has made a series of smaller filings under the state law in the past year, including notices covering 121 jobs in August, 57 tech jobs earlier in the summer and 84 jobs last December.

The latest notice covers employees at 17 Amazon offices and warehouses in Seattle, Bellevue, Redmond, Sumner and Pasco, plus 15 remote workers in the state.

The list also includes risk managers, several directors and other senior employees. Employees who land other jobs inside Amazon before their separation dates won’t be laid off, the notice says.

The Stores cuts were much smaller than Amazon’s major corporate cutbacks since last fall. The company eliminated about 14,000 corporate roles globally in October 2025, including 2,303 in Washington, followed by another round in January that included 2,198 Washington employees.

Read the whole story
alvinashcraft
46 seconds ago
reply
Pennsylvania, USA
Share this story
Delete

Don’t miss Seattle tech’s biggest holiday party: Get early bird tickets for the GeekWire Gala

1 Share
Scenes from a previous GeekWire Gala in Seattle. Tickets are now on sale for the big bash on Dec. 10. (GeekWire File Photo)

Early bird tickets for the 2026 GeekWire Gala are on sale now. Join us Dec. 10 for Seattle tech’s biggest holiday celebration, with a new venue, fresh experiences and hundreds of the region’s top founders, investors, entrepreneurs and business leaders.

This year, we’re taking the party to The Shop, an exclusive SoDo venue featuring an extraordinary collection of classic and exotic cars. It’s a new setting for the annual celebration, with several new activations planned throughout the night.

The GeekWire Gala is part holiday party, part reunion and part networking extravaganza. Expect food, drinks, signature cocktails, games, music, dancing and plenty of opportunities to catch up with old friends and make new connections. Bring your colleagues, clients, friends or entire team. You can even make it your company’s holiday party this year.

We’ll also bring back our Uncommon Thinkers program, recognizing the creative minds and unconventional builders shaping the future of the Pacific Northwest. Presented in partnership with Greater Seattle Partners, the program shines a spotlight on innovators making a difference across our region.

In partnership with Microsoft, we’ll also be raising awareness this year for six amazing non-profits that help make our region a better place to live.

The festivities take place Thursday, Dec. 10, with the main event running from 6 p.m. to 10 p.m. at The Shop at 2233 6th Ave. S. in Seattle’s SoDo neighborhood.

Dress to impress, break out your holiday flair, or keep it geek chic. Cocktail attire is welcome, but there are no fashion rules at this party.

A big thank you to our returning presenting sponsor, First Tech Federal Credit Union, and our 2026 sponsors, including Microsoft, Wilson Sonsini, Zoox, University of Washington, ALLtech, WTIA, Turkish Airlines, Washington State University and Prime Team Partners. We’re also grateful to Greater Seattle Partners for its continued partnership on Uncommon Thinkers.

Get your GeekWire Gala tickets here and make plans to celebrate the year with Seattle’s tech community. Sponsorships and group ticket packages are also available. Contact us at events@geekwire.com for details.

Read the whole story
alvinashcraft
1 minute ago
reply
Pennsylvania, USA
Share this story
Delete
Next Page of Stories