diff --git a/README.md b/README.md index 3cc7f6b..bd2382f 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ You can keep both subprojects in the same repo if your package ships SDKs in bot ## What's in each template -Both templates expose the same seven Strands extension points so you can pick whichever fits your use case: +Both templates expose the same eight Strands extension points so you can pick whichever fits your use case: | Component | Purpose | |----------------------|--------------------------------------------------------| @@ -45,6 +45,7 @@ Both templates expose the same seven Strands extension points so you can pick wh | Session manager | Persist conversations across restarts | | Conversation manager | Control context window and message history | | Memory store | Give agents cross-session knowledge via a search backend | +| Storage | Persist raw bytes under string keys for sessions, offloading, etc. | The interactive setup script in each subdirectory removes the components you don't select, renames everything to your package name, and wires up `pyproject.toml` / `package.json` accordingly. diff --git a/python/setup_template.py b/python/setup_template.py index f2bb399..c2ac20a 100644 --- a/python/setup_template.py +++ b/python/setup_template.py @@ -70,6 +70,13 @@ "test_files": ["test_memory_store.py"], "exports": ["TemplateMemoryStore"], }, + "storage": { + "name": "Storage", + "description": "Persist raw bytes under string keys for sessions, offloading, etc.", + "files": ["storage.py"], + "test_files": ["test_storage.py"], + "exports": ["TemplateStorage"], + }, } @@ -437,6 +444,7 @@ def main() -> None: "TemplateSessionManager": f"{pascal_name}SessionManager", "TemplateConversationManager": f"{pascal_name}ConversationManager", "TemplateMemoryStore": f"{pascal_name}MemoryStore", + "TemplateStorage": f"{pascal_name}Storage", # Function names "template_tool": f"{snake_name}_tool", # Plugin name diff --git a/python/src/strands_template/__init__.py b/python/src/strands_template/__init__.py index a5bdca1..3b5d9a8 100644 --- a/python/src/strands_template/__init__.py +++ b/python/src/strands_template/__init__.py @@ -6,6 +6,7 @@ from strands_template.model import TemplateModel from strands_template.plugin import TemplatePlugin from strands_template.session_manager import TemplateSessionManager +from strands_template.storage import TemplateStorage from strands_template.tool import template_tool __all__ = [ @@ -16,4 +17,5 @@ "TemplateModel", "TemplatePlugin", "TemplateSessionManager", + "TemplateStorage", ] diff --git a/python/src/strands_template/storage.py b/python/src/strands_template/storage.py new file mode 100644 index 0000000..95aabb8 --- /dev/null +++ b/python/src/strands_template/storage.py @@ -0,0 +1,72 @@ +"""Storage Implementation.""" + +import builtins +import logging + +logger = logging.getLogger(__name__) + + +class TemplateStorage: + """Template storage backend implementation. + + Storage backends persist raw bytes under string keys. The SDK uses the + Storage interface for session snapshots, context offloading, and any + construct that needs durable key-value persistence. + + Example: + ```python + from strands import Agent + from strands.session import SessionManager + from strands_template import TemplateStorage + + storage = TemplateStorage() + agent = Agent(session_manager=SessionManager(storage=storage)) + ``` + """ + + def __init__(self) -> None: + """Initialize the storage backend.""" + # TODO: Add backend-specific constructor arguments. + + async def write(self, key: str, data: bytes) -> None: + """Store data under key, overwriting any existing value. + + Args: + key: Opaque, '/'-separated key identifying the value. + data: Raw bytes to persist. + """ + # TODO: Persist the data to your backend. + raise NotImplementedError + + async def read(self, key: str) -> bytes | None: + """Retrieve the bytes previously stored under key. + + Args: + key: The key to read. + + Returns: + The stored bytes, or None if no value exists for key. + """ + # TODO: Read from your backend, return None if not found. + raise NotImplementedError + + async def delete(self, key: str) -> None: + """Delete the value stored under key. A no-op if the key does not exist. + + Args: + key: The key to delete. + """ + # TODO: Delete from your backend. + raise NotImplementedError + + async def list(self, query: str = "") -> builtins.list[str]: + """List keys matching the given prefix query. + + Args: + query: A prefix string to filter keys. Empty string matches all. + + Returns: + Matching keys sorted ascending. + """ + # TODO: Query your backend for keys matching the prefix. + raise NotImplementedError diff --git a/python/tests/test_storage.py b/python/tests/test_storage.py new file mode 100644 index 0000000..8f4a864 --- /dev/null +++ b/python/tests/test_storage.py @@ -0,0 +1,17 @@ +"""Tests for TemplateStorage.""" + +from strands.storage import Storage + +from strands_template import TemplateStorage + + +def test_template_storage_init(): + """Test initialization.""" + storage = TemplateStorage() + assert isinstance(storage, TemplateStorage) + + +def test_satisfies_storage_protocol(): + """TemplateStorage satisfies the Storage protocol.""" + storage = TemplateStorage() + assert isinstance(storage, Storage) diff --git a/typescript/package-lock.json b/typescript/package-lock.json index b75e23a..325070c 100644 --- a/typescript/package-lock.json +++ b/typescript/package-lock.json @@ -9,7 +9,7 @@ "version": "0.0.0", "license": "Apache-2.0", "devDependencies": { - "@strands-agents/sdk": ">=1.0.0 <2.0.0", + "@strands-agents/sdk": "^1.10.0", "@types/node": "^22.0.0", "@typescript-eslint/eslint-plugin": "^8.0.0", "@typescript-eslint/parser": "^8.0.0", @@ -1620,13 +1620,13 @@ "license": "MIT" }, "node_modules/@strands-agents/sdk": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@strands-agents/sdk/-/sdk-1.7.0.tgz", - "integrity": "sha512-/ds9Su6KBbBykfN456I958h4Kw5lJ29jniCzRmJFDPrxWDNShx50Mxng+tmuvyBHDCjWO8W7pvebYeFUkVkdQA==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@strands-agents/sdk/-/sdk-1.10.0.tgz", + "integrity": "sha512-7KIGM+6cCgN/RbIcXFRTtLkLkdtXB57zprUvy2bZL+BO8WkhHh4Fv8DpRwTAZXZQUq37KqwmQ/DyX70Y2xTvog==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@aws-sdk/client-bedrock-runtime": "^3.1069.0", + "@aws-sdk/client-bedrock-runtime": "^3.1078.0", "@types/json-schema": "^7.0.15", "uuid": "^14.0.1", "yaml": "^2.8.3" @@ -1637,10 +1637,10 @@ "peerDependencies": { "@a2a-js/sdk": "^0.3.10", "@ai-sdk/provider": "^3.0.0", - "@anthropic-ai/sdk": "^0.104.2", - "@aws-sdk/client-bedrock-agent": "^3.1075.0", - "@aws-sdk/client-bedrock-agent-runtime": "^3.1075.0", - "@aws-sdk/client-s3": "^3.1069.0", + "@anthropic-ai/sdk": "^0.109.1", + "@aws-sdk/client-bedrock-agent": "^3.1078.0", + "@aws-sdk/client-bedrock-agent-runtime": "^3.1078.0", + "@aws-sdk/client-s3": "^3.1078.0", "@aws/bedrock-token-generator": "^1.1.0", "@cedar-policy/cedar-wasm": "^4.0.0", "@cedar-policy/mcp-schema-generator-wasm": "^0.6.0", @@ -1653,9 +1653,9 @@ "@opentelemetry/sdk-metrics": "^2.6.1", "@opentelemetry/sdk-trace-base": "^2.6.1", "@opentelemetry/sdk-trace-node": "^2.6.1", - "@smithy/types": "^4.15.0", + "@smithy/types": "^4.15.1", "express": "^5.1.0", - "openai": "^6.42.0", + "openai": "^6.45.0", "zod": "^4.1.12" }, "peerDependenciesMeta": { diff --git a/typescript/package.json b/typescript/package.json index 8bed004..9af944b 100644 --- a/typescript/package.json +++ b/typescript/package.json @@ -54,7 +54,7 @@ "zod": "^4.1.12" }, "devDependencies": { - "@strands-agents/sdk": ">=1.0.0 <2.0.0", + "@strands-agents/sdk": "^1.10.0", "@types/node": "^22.0.0", "@typescript-eslint/eslint-plugin": "^8.0.0", "@typescript-eslint/parser": "^8.0.0", diff --git a/typescript/setup-template.ts b/typescript/setup-template.ts index c5cb53d..000e93c 100644 --- a/typescript/setup-template.ts +++ b/typescript/setup-template.ts @@ -84,6 +84,13 @@ const COMPONENTS: Record = { testFiles: ['memory-store.test.ts'], exports: ['TemplateMemoryStore', 'TemplateMemoryStoreConfig'], }, + storage: { + name: 'Storage', + description: 'Persist raw bytes under string keys for sessions, offloading, etc.', + files: ['storage.ts'], + testFiles: ['storage.test.ts'], + exports: ['TemplateStorage', 'TemplateStorageConfig'], + }, } const SRC_DIR = 'src' @@ -304,6 +311,8 @@ function buildReplacements(info: ProjectInfo): Record { TemplateConversationManager: `${pascalName}ConversationManager`, TemplateMemoryStoreConfig: `${pascalName}MemoryStoreConfig`, TemplateMemoryStore: `${pascalName}MemoryStore`, + TemplateStorageConfig: `${pascalName}StorageConfig`, + TemplateStorage: `${pascalName}Storage`, // Tool function name templateTool: `${camelName}Tool`, template_tool: `${camelName.replace(/([A-Z])/g, '_$1').toLowerCase()}_tool`, diff --git a/typescript/src/index.ts b/typescript/src/index.ts index 7095078..ee562fd 100644 --- a/typescript/src/index.ts +++ b/typescript/src/index.ts @@ -9,3 +9,4 @@ export { TemplateIntervention } from './intervention.js' export { TemplateSnapshotStorage, type TemplateSnapshotStorageConfig } from './session-manager.js' export { TemplateConversationManager } from './conversation-manager.js' export { TemplateMemoryStore, type TemplateMemoryStoreConfig } from './memory-store.js' +export { TemplateStorage, type TemplateStorageConfig } from './storage.js' diff --git a/typescript/src/storage.ts b/typescript/src/storage.ts new file mode 100644 index 0000000..3458761 --- /dev/null +++ b/typescript/src/storage.ts @@ -0,0 +1,51 @@ +/** + * Storage Implementation. + * + * Storage backends persist raw bytes under string keys. The SDK uses the + * {@link Storage} interface for session snapshots, context offloading, and any + * construct that needs durable key-value persistence. + * + * @example + * ```ts + * import { Agent, SessionManager } from '@strands-agents/sdk' + * import { TemplateStorage } from 'strands-template' + * + * const storage = new TemplateStorage({}) + * const session = new SessionManager({ + * sessionId: 'abc', + * storage: { snapshot: storage }, + * }) + * const agent = new Agent({ sessionManager: session }) + * ``` + */ + +import type { Storage } from '@strands-agents/sdk' + +// eslint-disable-next-line @typescript-eslint/no-empty-object-type +export interface TemplateStorageConfig { + // TODO: Add backend-specific fields your storage needs. +} + +export class TemplateStorage implements Storage { + constructor(private readonly _config: TemplateStorageConfig) {} + + async write(_key: string, _data: Uint8Array): Promise { + // TODO: Persist the data to your backend. + throw new Error('Not implemented') + } + + async read(_key: string): Promise { + // TODO: Read from your backend, return null if not found. + throw new Error('Not implemented') + } + + async delete(_key: string): Promise { + // TODO: Delete from your backend. No-op if key does not exist. + throw new Error('Not implemented') + } + + async list(_query: string): Promise { + // TODO: Return keys matching the prefix, sorted ascending. + throw new Error('Not implemented') + } +} diff --git a/typescript/test/storage.test.ts b/typescript/test/storage.test.ts new file mode 100644 index 0000000..4817e82 --- /dev/null +++ b/typescript/test/storage.test.ts @@ -0,0 +1,15 @@ +import { describe, it, expect } from 'vitest' +import type { Storage } from '@strands-agents/sdk' +import { TemplateStorage } from '../src/storage.js' + +describe('TemplateStorage', () => { + it('satisfies the Storage interface', () => { + const storage: Storage = new TemplateStorage({}) + expect(typeof storage.write).toBe('function') + expect(typeof storage.read).toBe('function') + expect(typeof storage.delete).toBe('function') + expect(typeof storage.list).toBe('function') + }) + + // TODO: Add round-trip write/read tests once the storage backend is implemented. +})