Skip to content

feat(ts#agent): add session management support - #1017

Open
AlexTo wants to merge 4 commits into
awslabs:mainfrom
AlexTo:ts-agent-session-manager
Open

feat(ts#agent): add session management support#1017
AlexTo wants to merge 4 commits into
awslabs:mainfrom
AlexTo:ts-agent-session-manager

Conversation

@AlexTo

@AlexTo AlexTo commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

Reason for this change

ts#agent-generated agents had no built-in way to persist conversation state (message history, tool state) across invocations. Since Bedrock AgentCore Runtime can scale or restart between requests, this meant conversations couldn't survive beyond a single process lifetime.

Description of changes

  • Add a session option to ts#agent: s3 (default) or in-memory.
  • Generate a session.ts exporting getSessionManager(), wired into agent.ts for HTTP/A2A agents (directly on the Agent constructor) and into index.ts for AG-UI agents (via sessionManagerProvider, since AG-UI clones a template agent per thread).
  • When session: s3, CDK/Terraform provision a dedicated S3 bucket (SSE-S3 encrypted, all public access blocked) and grant the agent's IAM role read/write access; the bucket name is registered in AppConfig alongside the agent's runtime ARN.
  • Local development always uses LocalFileStorage under tmp/agents/strands/<agent-name> at the workspace root, regardless of the configured session option.
  • Add @aws-sdk/client-s3 as an explicit dependency when session: s3, so the bundler can resolve the Strands SDK's lazy import('@aws-sdk/client-s3') — without it, the deployed container (which ships no node_modules) fails at runtime.
  • Add a ts-agent-session-management-support Nx migration to bring existing (pre-feature) workspaces up to the new shape: reshapes agentRuntimes entries to { arn, session }, backfills session.ts and wires it into existing agents, and migrates the AG-UI error-logging hooks to StrandsAgent plugins.
  • py#agent also gains the session option (schema only, always in-memory) for API parity — Python session manager support isn't implemented yet.
  • Docs: new "Session Management" section in the ts#agent guide; fixed stale FileTree/code samples that predated this change.

Description of how you validated changes

  • Unit tests.
  • Generated a baseline test project with the pre-session-feature version of ts#agent/py#agent, including MCP server, A2A, and React website connections, in this commit.
  • Applied the migration in this commit to verify existing HTTP/A2A/AG-UI agents (TypeScript and Python), along with their MCP/A2A/React connections, were correctly upgraded — session.ts backfilled and wired in, error-logging hooks migrated to StrandsAgent plugins for the AG-UI agent, agentRuntimes reshaped, and connection client/provider code updated to read the new { arn, session } shape.
  • Generated new agents with session: s3, an A2A connection between them, and wired them to CDK infra in this commit, deployed to AWS, and verified the sessionId flows through end-to-end — each connected agent persists its session under the same sessionId, in its own dedicated S3 bucket. Also chatted with the agents locally to verify each agent's local session directory (tmp/agents/strands/<agent-name>) is created and populated correctly.

Issue # (if applicable)

Closes #.

Checklist


By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license

AlexTo added 4 commits August 2, 2026 14:27
S3Storage lazily imports @aws-sdk/client-s3 at runtime; without it declared,
the bundler leaves the import unresolved and it fails in the deployed
container, which only ships the bundled output (no node_modules).
Matches the codebase's dash-separated multi-word option convention
(ag-ui, cloudfront-s3, etc.) and reads more clearly at the call site
than 'none', which could be mistaken for "no session support at all"
rather than "session kept in process memory only".
@codecov-commenter

codecov-commenter commented Aug 2, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 84.74576% with 27 lines in your changes missing coverage. Please review.
✅ Project coverage is 88.43%. Comparing base (5d440b4) to head (2aef3e1).

Files with missing lines Patch % Lines
...t/ts-agent-session-management-support/migration.ts 83.53% 7 Missing and 20 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1017      +/-   ##
==========================================
- Coverage   88.53%   88.43%   -0.10%     
==========================================
  Files         179      180       +1     
  Lines        6666     6842     +176     
  Branches     1619     1672      +53     
==========================================
+ Hits         5902     6051     +149     
- Misses        364      371       +7     
- Partials      400      420      +20     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@cogwirrel cogwirrel left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great, thanks @AlexTo! :) Few minor comments :)

Comment on lines +363 to +369
```typescript
export const getSessionManager = async (): Promise<SessionManager> => {
const sessionId = getCurrentSessionId();
// local development always uses local file storage...
// ...otherwise S3Storage (session: 's3') or InMemoryStorage (session: 'in-memory')
};
```

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: probably don't need this example :)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removed

Comment on lines +658 to +659
autoDeleteObjects: true,
removalPolicy: RemovalPolicy.DESTROY,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While useful for testing, I think it's safer not to delete data on destroy, like we do for databases :)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated

Comment on lines +668 to +672
suppressRules(
sessionBucket,
['CKV_AWS_145'],
'AES256 (S3-managed) encryption is sufficient for session data',
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably better to use a CMK :)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Comment on lines +688 to +692
suppressRules(
sessionBucket,
['CKV_AWS_18'],
'Access logging not required for session data',
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should follow the best practice and store access logs in cloudwatch like for other buckets :)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Comment on lines +862 to +868
#checkov:skip=CKV2_AWS_61:Lifecycle configuration not required for session data
#checkov:skip=CKV_AWS_144:Cross-region replication not required for session data
#checkov:skip=CKV2_AWS_62:Event notifications not required for session data
#checkov:skip=CKV_AWS_18:Access logging not required for session data
#checkov:skip=CKV_AWS_21:Session data does not need versioning enabled
#checkov:skip=CKV_AWS_145:AES256 (S3-managed) encryption is sufficient for session data
force_destroy = true

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comments as above for s3, best to retain data, use a cmk and store s3 access logs in cloudwatch :)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Comment on lines +95 to +105
// Local-dev session storage lives at the workspace root
// (`tmp/agents/strands/<agent-name>`), not inside the project, so each agent
// gets its own storage directory. The `-dev`/`-serve` targets run with
// cwd={projectRoot}, so compute that directory relative to the project root
// here rather than resolving it at runtime (e.g. via import.meta.url),
// which would need an extra runtime helper.
const projectDepth = project.root.split('/').filter(Boolean).length;
const localSessionsDir = joinPathFragments(
Array(projectDepth).fill('..').join('/'),
`tmp/agents/strands/${name}`,
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: you can use getRelativePathToRootByDirectory :)

export const getRelativePathToRootByDirectory = (directory: string): string => {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

* Plugin that logs model invocation errors, calling out missing credentials or
* denied permissions.
*/
export class ModelErrorLoggingPlugin implements Plugin {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice one! :)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

actually HTTP/A2A should also use the plugin form instead of calling log*Errors, let me update it

<%- nameClassName %>: {
arn: this.agentCoreRuntime.agentRuntimeArn,
session: {
storage: '<%- session %>',

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to store storage? I might have missed it but it doesn't look like anything actually checks this, as the getSessionManager is generated to look for the bucketName regardless :)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah actually no, it is just metadata. removed to simplify the config

Comment on lines +110 to +114
// A string value is a local-dev override URL; otherwise it's the
// deployed runtime entry, so build the invocation URL from its ARN.
typeof agentRuntimeValue === 'string'
? agentRuntimeValue
: buildAgentCoreUrl(agentRuntimeValue.arn),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we get a type when the connection generator sets the value which is meant to be an object as a string? :)

@AlexTo AlexTo Aug 2, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think no, because IRuntimeConfig is any
image

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also realised that the shape of the client side connection doesn't have to follow the shape of the server side agentcore config so it can remain as agentRuntimes: string. But maybe it's a good idea to just refactor it into an object agentRuntimes: { arn: string } to be consistent and in case we have to include other settings in the future?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ooh yes true- I think it might be best to keep as a string to keep it simple, we can always refactor later if needed :) probably better to have a bit of friction to do so too as we ideally don’t want to send the bucket name to the front end :)

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.

3 participants