diff --git a/.gitignore b/.gitignore index 9e8fd80d..ce1ae621 100644 --- a/.gitignore +++ b/.gitignore @@ -57,3 +57,5 @@ playwright-report/ # Generated CLI skills **/.agents/skills/ +apps/testing/ag-auth-test-app/cookies.txt +apps/testing/ag-auth-test-app/agentuity.json diff --git a/AGENTS.md b/AGENTS.md index 0ea1c754..01cb463a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -43,6 +43,7 @@ See [docs/testing.md](docs/testing.md) for detailed standards. - Import from `../src/` in tests - Use `@agentuity/test-utils` for mocks - All errors AND warnings must be zero +- When running tests, prefer using a subagent (Task tool) to avoid context bloat from test output ## Special Instructions diff --git a/apps/testing/ag-auth-test-app/.agents/agentuity/sdk/agent/AGENTS.md b/apps/testing/ag-auth-test-app/.agents/agentuity/sdk/agent/AGENTS.md new file mode 100644 index 00000000..65412abc --- /dev/null +++ b/apps/testing/ag-auth-test-app/.agents/agentuity/sdk/agent/AGENTS.md @@ -0,0 +1,308 @@ +# Agents Folder Guide + +This folder contains AI agents for your Agentuity application. Each agent is organized in its own subdirectory. + +## Generated Types + +The `src/generated/` folder contains auto-generated TypeScript files: + +- `registry.ts` - Agent registry with strongly-typed agent definitions and schema types +- `routes.ts` - Route registry for API, WebSocket, and SSE endpoints +- `app.ts` - Application entry point (regenerated on every build) + +**Important:** Never edit files in `src/generated/` - they are overwritten on every build. + +Import generated types in your agents: + +```typescript +import type { HelloInput, HelloOutput } from '../generated/registry'; +``` + +## Directory Structure + +Each agent folder must contain: + +- **agent.ts** (required) - Agent definition with schema and handler + +Example structure: + +``` +src/agent/ +├── hello/ +│ └── agent.ts +├── process-data/ +│ └── agent.ts +└── (generated files in src/generated/) +``` + +**Note:** HTTP routes are defined separately in `src/api/` - see the API folder guide for details. + +## Creating an Agent + +### Basic Agent (agent.ts) + +```typescript +import { createAgent } from '@agentuity/runtime'; +import { s } from '@agentuity/schema'; + +const agent = createAgent('my-agent', { + description: 'What this agent does', + schema: { + input: s.object({ + name: s.string(), + age: s.number(), + }), + output: s.string(), + }, + handler: async (ctx, input) => { + // Access context: ctx.app, ctx.config, ctx.logger, ctx.kv, ctx.vector, ctx.stream + return `Hello, ${input.name}! You are ${input.age} years old.`; + }, +}); + +export default agent; +``` + +### Agent with Lifecycle (setup/shutdown) + +```typescript +import { createAgent } from '@agentuity/runtime'; +import { s } from '@agentuity/schema'; + +const agent = createAgent('lifecycle-agent', { + description: 'Agent with setup and shutdown', + schema: { + input: s.object({ message: s.string() }), + output: s.object({ result: s.string() }), + }, + setup: async (app) => { + // Initialize resources (runs once on startup) + // app contains: appName, version, startedAt, config + return { + agentId: `agent-${Math.random().toString(36).substr(2, 9)}`, + connectionPool: ['conn-1', 'conn-2'], + }; + }, + handler: async (ctx, input) => { + // Access setup config via ctx.config (fully typed) + ctx.logger.info('Agent ID:', ctx.config.agentId); + ctx.logger.info('Connections:', ctx.config.connectionPool); + return { result: `Processed: ${input.message}` }; + }, + shutdown: async (app, config) => { + // Cleanup resources (runs on shutdown) + console.log('Shutting down agent:', config.agentId); + }, +}); + +export default agent; +``` + +### Agent with Event Listeners + +```typescript +import { createAgent } from '@agentuity/runtime'; +import { s } from '@agentuity/schema'; + +const agent = createAgent('event-agent', { + schema: { + input: s.object({ data: s.string() }), + output: s.string(), + }, + handler: async (ctx, input) => { + return `Processed: ${input.data}`; + }, +}); + +agent.addEventListener('started', (eventName, agent, ctx) => { + ctx.logger.info('Agent started'); +}); + +agent.addEventListener('completed', (eventName, agent, ctx) => { + ctx.logger.info('Agent completed'); +}); + +agent.addEventListener('errored', (eventName, agent, ctx, error) => { + ctx.logger.error('Agent errored:', error); +}); + +export default agent; +``` + +## Agent Context (ctx) + +The handler receives a context object with: + +- **ctx.app** - Application state (appName, version, startedAt, config from createApp) +- **ctx.config** - Agent-specific config (from setup return value, fully typed) +- **ctx.logger** - Structured logger (info, warn, error, debug, trace) +- **ctx.tracer** - OpenTelemetry tracer for custom spans +- **ctx.sessionId** - Unique session identifier +- **ctx.kv** - Key-value storage +- **ctx.vector** - Vector storage for embeddings +- **ctx.stream** - Stream storage for real-time data +- **ctx.state** - In-memory request-scoped state (Map) +- **ctx.thread** - Thread information for multi-turn conversations +- **ctx.session** - Session information +- **ctx.waitUntil** - Schedule background tasks + +## Examples + +### Using Key-Value Storage + +```typescript +handler: async (ctx, input) => { + await ctx.kv.set('user:123', { name: 'Alice', age: 30 }); + const user = await ctx.kv.get('user:123'); + await ctx.kv.delete('user:123'); + const keys = await ctx.kv.list('user:*'); + return user; +}; +``` + +### Using Vector Storage + +```typescript +handler: async (ctx, input) => { + await ctx.vector.upsert('docs', [ + { id: '1', values: [0.1, 0.2, 0.3], metadata: { text: 'Hello' } }, + ]); + const results = await ctx.vector.query('docs', [0.1, 0.2, 0.3], { topK: 5 }); + return results; +}; +``` + +### Using Streams + +```typescript +handler: async (ctx, input) => { + const stream = await ctx.stream.create('agent-logs'); + await ctx.stream.write(stream.id, 'Processing step 1'); + await ctx.stream.write(stream.id, 'Processing step 2'); + return { streamId: stream.id }; +}; +``` + +### Background Tasks with waitUntil + +```typescript +handler: async (ctx, input) => { + // Schedule background work that continues after response + ctx.waitUntil(async () => { + await ctx.kv.set('processed', Date.now()); + ctx.logger.info('Background task complete'); + }); + + return { status: 'processing' }; +}; +``` + +### Calling Another Agent + +```typescript +// Import the agent directly +import otherAgent from '../other-agent/agent'; + +handler: async (ctx, input) => { + const result = await otherAgent.run({ data: input.value }); + return `Other agent returned: ${result}`; +}; +``` + +## Subagents (Nested Agents) + +Agents can have subagents organized one level deep. This is useful for grouping related functionality. + +### Directory Structure for Subagents + +``` +src/agent/ +└── team/ # Parent agent + ├── agent.ts # Parent agent + ├── members/ # Subagent + │ └── agent.ts + └── tasks/ # Subagent + └── agent.ts +``` + +### Parent Agent + +```typescript +import { createAgent } from '@agentuity/runtime'; +import { s } from '@agentuity/schema'; + +const agent = createAgent('team', { + description: 'Team Manager', + schema: { + input: s.object({ action: s.union([s.literal('info'), s.literal('count')]) }), + output: s.object({ + message: s.string(), + timestamp: s.string(), + }), + }, + handler: async (ctx, { action }) => { + return { + message: 'Team parent agent - manages members and tasks', + timestamp: new Date().toISOString(), + }; + }, +}); + +export default agent; +``` + +### Subagent (Accessing Parent) + +```typescript +import { createAgent } from '@agentuity/runtime'; +import { s } from '@agentuity/schema'; +import parentAgent from '../agent'; + +const agent = createAgent('team.members', { + description: 'Members Subagent', + schema: { + input: s.object({ + action: s.union([s.literal('list'), s.literal('add'), s.literal('remove')]), + name: s.optional(s.string()), + }), + output: s.object({ + members: s.array(s.string()), + parentInfo: s.optional(s.string()), + }), + }, + handler: async (ctx, { action, name }) => { + // Call parent agent directly + const parentResult = await parentAgent.run({ action: 'info' }); + const parentInfo = `Parent says: ${parentResult.message}`; + + let members = ['Alice', 'Bob']; + if (action === 'add' && name) { + members.push(name); + } + + return { members, parentInfo }; + }, +}); + +export default agent; +``` + +### Key Points About Subagents + +- **One level deep**: Only one level of nesting is supported (no nested subagents) +- **Access parent**: Import and call parent agents directly +- **Agent names**: Subagents have dotted names like `"team.members"` +- **Shared context**: Subagents share the same app context (kv, logger, etc.) + +## Rules + +- Each agent folder name becomes the agent's route name (e.g., `hello/` → `/agent/hello`) +- **agent.ts** must export default the agent instance +- The first argument to `createAgent()` is the agent name (must match folder structure) +- Input/output schemas are enforced with @agentuity/schema validation +- Setup return value type automatically flows to ctx.config (fully typed) +- Use ctx.logger for logging, not console.log +- Import agents directly to call them (recommended approach) +- Subagents are one level deep only (team/members/, not team/members/subagent/) + + \ No newline at end of file diff --git a/apps/testing/ag-auth-test-app/.agents/agentuity/sdk/api/AGENTS.md b/apps/testing/ag-auth-test-app/.agents/agentuity/sdk/api/AGENTS.md new file mode 100644 index 00000000..7703c74a --- /dev/null +++ b/apps/testing/ag-auth-test-app/.agents/agentuity/sdk/api/AGENTS.md @@ -0,0 +1,362 @@ +# APIs Folder Guide + +This folder contains REST API routes for your Agentuity application. Each API is organized in its own subdirectory. + +## Generated Types + +The `src/generated/` folder contains auto-generated TypeScript files: + +- `routes.ts` - Route registry with strongly-typed route definitions and schema types +- `registry.ts` - Agent registry (for calling agents from routes) +- `app.ts` - Application entry point (regenerated on every build) + +**Important:** Never edit files in `src/generated/` - they are overwritten on every build. + +Import generated types in your routes: + +```typescript +import type { POST_Api_UsersInput, POST_Api_UsersOutput } from '../generated/routes'; +``` + +## Directory Structure + +Each API folder must contain: + +- **route.ts** (required) - HTTP route definitions using Hono router + +Example structure: + +``` +src/api/ +├── index.ts (optional, mounted at /api) +├── status/ +│ └── route.ts (mounted at /api/status) +├── users/ +│ └── route.ts (mounted at /api/users) +├── agent-call/ + └── route.ts (mounted at /api/agent-call) +``` + +## Creating an API + +### Basic API (route.ts) + +```typescript +import { createRouter } from '@agentuity/runtime'; + +const router = createRouter(); + +// GET /api/status +router.get('/', (c) => { + return c.json({ + status: 'ok', + timestamp: new Date().toISOString(), + version: '1.0.0', + }); +}); + +// POST /api/status +router.post('/', async (c) => { + const body = await c.req.json(); + return c.json({ received: body }); +}); + +export default router; +``` + +### API with Request Validation + +```typescript +import { createRouter } from '@agentuity/runtime'; +import { s } from '@agentuity/schema'; +import { validator } from 'hono/validator'; + +const router = createRouter(); + +const createUserSchema = s.object({ + name: s.string(), + email: s.string(), + age: s.number(), +}); + +router.post( + '/', + validator('json', (value, c) => { + const result = createUserSchema['~standard'].validate(value); + if (result.issues) { + return c.json({ error: 'Validation failed', issues: result.issues }, 400); + } + return result.value; + }), + async (c) => { + const data = c.req.valid('json'); + // data is fully typed: { name: string, email: string, age: number } + return c.json({ + success: true, + user: data, + }); + } +); + +export default router; +``` + +### API Calling Agents + +APIs can call agents directly by importing them: + +```typescript +import { createRouter } from '@agentuity/runtime'; +import helloAgent from '@agent/hello'; + +const router = createRouter(); + +router.get('/', async (c) => { + // Call an agent directly + const result = await helloAgent.run({ name: 'API Caller', age: 42 }); + + return c.json({ + success: true, + agentResult: result, + }); +}); + +router.post('/with-input', async (c) => { + const body = await c.req.json(); + const { name, age } = body; + + // Call agent with dynamic input + const result = await helloAgent.run({ name, age }); + + return c.json({ + success: true, + agentResult: result, + }); +}); + +export default router; +``` + +### API with Agent Validation + +Use `agent.validator()` for automatic input validation from agent schemas: + +```typescript +import { createRouter } from '@agentuity/runtime'; +import myAgent from '@agent/my-agent'; + +const router = createRouter(); + +// POST with automatic validation using agent's input schema +router.post('/', myAgent.validator(), async (c) => { + const data = c.req.valid('json'); // Fully typed from agent schema! + const result = await myAgent.run(data); + return c.json({ success: true, result }); +}); + +export default router; +``` + +### API with Logging + +```typescript +import { createRouter } from '@agentuity/runtime'; + +const router = createRouter(); + +router.get('/log-test', (c) => { + c.var.logger.info('Info message'); + c.var.logger.error('Error message'); + c.var.logger.warn('Warning message'); + c.var.logger.debug('Debug message'); + c.var.logger.trace('Trace message'); + + return c.text('Check logs'); +}); + +export default router; +``` + +## Route Context (c) + +The route handler receives a Hono context object with: + +- **c.req** - Request object (c.req.json(), c.req.param(), c.req.query(), etc.) +- **c.json()** - Return JSON response +- **c.text()** - Return text response +- **c.html()** - Return HTML response +- **c.redirect()** - Redirect to URL +- **c.var.logger** - Structured logger (info, warn, error, debug, trace) +- **c.var.kv** - Key-value storage +- **c.var.vector** - Vector storage +- **c.var.stream** - Stream management +- **Import agents directly** - Import and call agents directly (recommended) + +## HTTP Methods + +```typescript +const router = createRouter(); + +router.get('/path', (c) => { + /* ... */ +}); +router.post('/path', (c) => { + /* ... */ +}); +router.put('/path', (c) => { + /* ... */ +}); +router.patch('/path', (c) => { + /* ... */ +}); +router.delete('/path', (c) => { + /* ... */ +}); +router.options('/path', (c) => { + /* ... */ +}); +``` + +## Path Parameters + +```typescript +// GET /api/users/:id +router.get('/:id', (c) => { + const id = c.req.param('id'); + return c.json({ userId: id }); +}); + +// GET /api/posts/:postId/comments/:commentId +router.get('/:postId/comments/:commentId', (c) => { + const postId = c.req.param('postId'); + const commentId = c.req.param('commentId'); + return c.json({ postId, commentId }); +}); +``` + +## Query Parameters + +```typescript +// GET /api/search?q=hello&limit=10 +router.get('/search', (c) => { + const query = c.req.query('q'); + const limit = c.req.query('limit') || '20'; + return c.json({ query, limit: parseInt(limit) }); +}); +``` + +## Request Body + +```typescript +// JSON body +router.post('/', async (c) => { + const body = await c.req.json(); + return c.json({ received: body }); +}); + +// Form data +router.post('/upload', async (c) => { + const formData = await c.req.formData(); + const file = formData.get('file'); + return c.json({ fileName: file?.name }); +}); +``` + +## Error Handling + +```typescript +import myAgent from '@agent/my-agent'; + +router.get('/', async (c) => { + try { + const result = await myAgent.run({ data: 'test' }); + return c.json({ success: true, result }); + } catch (error) { + c.var.logger.error('Agent call failed:', error); + return c.json( + { + success: false, + error: error instanceof Error ? error.message : String(error), + }, + 500 + ); + } +}); +``` + +## Response Types + +```typescript +// JSON response +return c.json({ data: 'value' }); + +// Text response +return c.text('Hello World'); + +// HTML response +return c.html('
Created: {result.name}
} +Status: {isConnected ? 'Connected' : 'Disconnected'}
+ +{JSON.stringify(msg)}
+ ))} +Connected: {isConnected ? 'Yes' : 'No'}
+ {error &&Error: {error.message}
} +Latest: {JSON.stringify(data)}
+ +API Base: {baseUrl}
; +} +``` + +### useAuth - Authentication State + +Access and manage authentication state. + +```typescript +import { useAuth } from '@agentuity/react'; + +function AuthStatus() { + const { isAuthenticated, authHeader, setAuthHeader, authLoading } = useAuth(); + + const handleLogin = async (token: string) => { + setAuthHeader?.(`Bearer ${token}`); + }; + + const handleLogout = () => { + setAuthHeader?.(null); + }; + + if (authLoading) returnLoading...
; + + return ( +Count: {count}
+ +{JSON.stringify(agentResult)}
+