Skip to content

Feat: Support ttl and expireTime session-expiration options in VertexAiSessionService.createSession - #561

Open
AmaadMartin wants to merge 6 commits into
google:mainfrom
AmaadMartin:feat/vertex-session-ttl-expire-time
Open

Feat: Support ttl and expireTime session-expiration options in VertexAiSessionService.createSession#561
AmaadMartin wants to merge 6 commits into
google:mainfrom
AmaadMartin:feat/vertex-session-ttl-expire-time

Conversation

@AmaadMartin

Copy link
Copy Markdown
Collaborator

Please ensure you have read the contribution guide before creating a pull request.

Link to Issue or Description of Change

1. Link to an existing issue (if applicable):

No existing issue.

2. Or, if no issue exists, describe the change:

Problem:
VertexAiSessionService.createSession forwards only sessionState and sessionId to the Agent Engine Sessions API, so every session is created with the service default lifetime. There is no way to create a session that expires after a relative ttl (e.g. '7200s') or at an absolute expireTime (e.g. '2025-10-01T00:00:00Z'), even though the Agent Engine Sessions resource supports both and adk-python's create_session already exposes them. This is a cross-language parity gap.

Solution:
Accept both options on the Vertex service and forward them verbatim into the config passed to the Sessions client:

const service = new VertexAiSessionService({
  projectId: 'my-project',
  location: 'us-central1',
  agentEngineId: '12345',
});

// Expire two hours after creation.
await service.createSession({appName: '12345', userId: 'user-1', ttl: '7200s'});

// Or expire at a fixed instant.
await service.createSession({
  appName: '12345',
  userId: 'user-1',
  expireTime: '2025-10-01T00:00:00Z',
});

Design notes:

  • The shared CreateSessionRequest is deliberately untouched. ttl/expireTime are Agent Engine concepts; InMemorySessionService and DatabaseSessionService have no way to honor them and would silently drop them. Instead the Vertex service narrows its override to a new VertexAiCreateSessionRequest extends CreateSessionRequest, exported from @google/adk so the public signature stays documentable. Narrowing a method parameter in a subclass override typechecks under --strict (TypeScript method parameters are bivariant), so no suppression of any kind was needed anywhere in this change.
  • Both at once throws before any RPC. The guard is the first statement of createSession, mirroring adk-python's ValueError, so an invalid appName cannot mask it and no request is issued.
  • ttl != null / expireTime != null (loose) is intentional, matching the sibling core/src/memory/vertex_ai_memory_bank_service.ts, which treats null and undefined identically as "not set" for these same two fields, and matching adk-python's is not None.
  • No new dependency and no version bump: @google-cloud/vertexai@1.12.0 (already pinned in the lockfile) declares ttl?: string and expireTime?: string on CreateAgentEngineSessionConfig, so the native SDK type is used as-is.
  • Known limitation (not addressed here): BaseSessionService.getOrCreateSession takes a CreateSessionRequest, so these options cannot be passed through it. Callers who need an expiring session must call createSession on a VertexAiSessionService reference directly; widening getOrCreateSession would mean changing the base contract for services that cannot honor it.
  • Reviewer note on style: the two new fields use the conditional-spread form (...(ttl != null ? {ttl} : {})) to match the two lines above them in the same literal and to keep the outgoing config free of undefined-valued keys. The trade-off is that TypeScript does not excess-property-check spread members, so a misnamed field would compile; that is what the wire-level test below is for (verified by mutation: renaming the key to ttlSeconds produces zero compiler errors but fails both a unit test and the wire test).

Reference: https://cloud.google.com/vertex-ai/generative-ai/docs/reference/rest/v1beta1/projects.locations.reasoningEngines.sessions

Testing Plan

Please describe the tests that you ran to verify your changes. This is required
for all PRs that are not small documentation or typo fixes.

Unit Tests:

  • I have added or updated unit tests for my change.
  • All unit tests pass locally.

Added to core/test/sessions/vertex_ai_session_service_test.ts (existing MockSessions fixture, no new mock):

  • forwards ttl to the create config — exact-equality on the create call, which also proves no expireTime key is emitted.
  • forwards expireTime to the create config — the mirror case.
  • throws when both ttl and expireTime are specified — asserts the message and expect(createInternal).not.toHaveBeenCalled(), pinning the guard ahead of the RPC.

Added tests/integration/sessions/vertex_ai_session_service_test.ts, which drives the real Sessions client against a loopback HTTP server and asserts the serialized request body ({userId, ttl} / {userId, expireTime}). Nothing is mocked; the only stub is the credential provider, since a loopback server has no identity to assert. This covers what the mocked unit tests structurally cannot: that the SDK actually serializes the two fields.

npm run build
npx vitest run --project unit:core core/test/sessions/vertex_ai_session_service_test.ts   # 54 passed
npx vitest run --project integration tests/integration/sessions/vertex_ai_session_service_test.ts   # 2 passed
npm run lint          # clean
npm run format:check  # clean
npm run docs:check    # clean (TypeDoc, warnings as errors)

New lines and branches in createSession are at 100% (v8 coverage over the changed file reports no uncovered statement and no partially-covered branch in the new range).

Manual End-to-End (E2E) Tests:

Please provide instructions on how to manually test your changes, including any
necessary setup or configuration.

Against a real Agent Engine instance (billable, therefore not automated):

import {VertexAiSessionService} from '@google/adk';

const service = new VertexAiSessionService({
  projectId: process.env.GOOGLE_CLOUD_PROJECT,
  location: process.env.GOOGLE_CLOUD_LOCATION,
  agentEngineId: process.env.AGENT_ENGINE_ID,
});

const session = await service.createSession({
  appName: process.env.AGENT_ENGINE_ID!,
  userId: 'u1',
  ttl: '7200s',
});

Then fetch the session through the Agent Engine Sessions REST API and confirm its expireTime is roughly two hours after its createTime. Repeat with expireTime: '<future RFC 3339 timestamp>' and confirm the value is stored verbatim. Passing both at once should reject locally with Cannot specify both 'ttl' and 'expireTime' simultaneously. without issuing a request.

Checklist

  • I have read the CONTRIBUTING.md document.
  • I have performed a self-review of my own code.
  • I have commented my code, particularly in hard-to-understand areas.
  • I have added tests that prove my fix is effective or that my feature works.
  • New and existing unit tests pass locally with my changes.
  • I have manually tested my changes end-to-end.
  • Any dependent changes have been merged and published in downstream modules.

Additional context

None.

Amaad Martin added 6 commits July 28, 2026 23:04
…createSession

Agent Engine Sessions accepts a relative ttl or an absolute expireTime when
creating a session, but createSession forwarded neither, so every session got
the service default lifetime. Add both as optional, Vertex-specific options via
a VertexAiCreateSessionRequest that extends CreateSessionRequest, keeping the
shared request type free of fields the in-memory and database services cannot
honor. Specifying both throws before any RPC, matching adk-python.
…body

The unit tests assert the config object handed to the SDK, which cannot catch a
field the SDK converter would drop. Drive the real Sessions client against a
loopback HTTP server and assert the serialized request body itself.
Drop the unit tests that re-asserted object-spread semantics with distinct
keys, keep the loopback test focused on the two wire-format cases the mocked
unit tests cannot reach, and stop restating the mutual-exclusion rule in three
doc comments.
State the mutual-exclusion rule once, assert what the explicit-undefined case
actually pins (that it does not throw, since toHaveBeenCalledWith ignores
undefined properties), and answer unparseable loopback requests so an
unexpected call fails an assertion instead of hanging.
createSession destructures its request, so {ttl: undefined} and an omitted ttl
produce identical locals - the case could only fail if createSession failed for
every caller. Correct the wire test's rationale to what it actually covers:
serialization by the SDK, which the mocked unit tests never reach.
The stub completes the operation inline so the poll loop never runs and only
one request reaches the server; the parse-failure branch and the close-error
branch could not be hit.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants