# PSADK

ps adk adapter

- [samples](https://pscode.lioncloud.net/agents/psadk_samples) : sample agents using PSADK

## Installation

```bash
$ npm install psadk
```

## Features

- **Multi-Provider LLM Support**: OpenAI, Anthropic, Google Gemini, AWS Bedrock, Azure OpenAI, and more
- **A2A Protocol Compatible**: Build agents that work with the Agent-to-Agent protocol
- **Local Agent Mode**: Run agents fully in-process via `agent-core` — no PS API dependency required
- **Prompt Caching**: Reduce costs and latency with built-in prompt caching support (90% cost reduction!)
- **Custom Tools**: Create and integrate custom tools with your agents
- **Type-Safe**: Full TypeScript support with comprehensive type definitions
- **Authentication**: Built-in JWT authentication and token management

## Documentation

The classes are documented here [Documentation](./globals.md)

**New:** [Prompt Caching Guide](./docs/PROMPT_CACHING.md) - Learn how to reduce costs and latency by 90% with prompt caching

**New:** [Local Agent Mode](#local-agent-mode) - Run agents in-process without the PS API

## Local Agent Mode

PSAgent now supports a **local execution mode** that runs the agent fully in-process using `agent-core`, bypassing the PS API entirely. 

LLM calls are routed through the PS LLM API — only the agent orchestration loop runs locally.

### Enabling Local Mode

Set `local: true` in your `PSAgentConfig` and add an `llm` entry to `ServiceUrls` pointing at your PS API base URL:

```typescript
import { PSAgent, PSAgentLogger } from 'psadk';
import type { LocalAgentEvent } from 'psadk';

const agent = new PSAgent(
  {
    name: 'MyAgent',
    description: 'A local agent',
    instruction: 'You are a helpful assistant.',
    model: 'gpt-4o-mini',
    token: process.env.PS_API_TOKEN,
    local: true,                    // ← enable local mode
    onEvent: (event: LocalAgentEvent) => {
      console.log(event.agentStatus, event.output?.response ?? event.message ?? '');
    },
  },
  new PSAgentLogger({ prefix: 'MyAgent' }),
  {
    agents: 'https://dev.lionis.ai',
    events: 'https://dev.lionis.ai',
    llm: 'https://dev.lionis.ai',   // ← LLM API base URL for local mode
  },
);

const result = await agent.start({ query: 'Hello!' });
console.log(result);
```

### Local Agent Configuration

| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `local` | `boolean` | `false` | Run the agent in-process via `agent-core` instead of the PS API |
| `onEvent` | `(event: LocalAgentEvent) => void` | — | Callback invoked for each streamed event during local execution |
| `ServiceUrls.llm` | `string` | agents URL | Base URL for the PS LLM API (e.g. `https://dev.lionis.ai`) |
| `llm_config.reasoning_effort` | `'minimal' \| 'low' \| 'medium' \| 'high'` | — | Controls model reasoning: `medium`/`high` → `reasoning: true`; `low`/`minimal` → `reasoning: false` |
| `llm_config.context_window` | `number` | `128000` | Context window size in tokens passed to the local agent runtime |
| `llm_config.max_completion_tokens` | `number` | `4096` | Max output tokens for the model |

### LocalAgentEvent Shape

```typescript
interface LocalAgentEvent {
  agentStatus: 'pending' | 'in-progress' | 'waiting' | 'completed' | 'failed';
  timestamp: number;        // Unix timestamp (ms)
  tool?: string;            // Present when agentStatus === 'waiting'
  toolInput?: { request_body: Record<string, any> }; // Tool input arguments
  output?: { response: string; [key: string]: any }; // Present on 'completed'
  message?: string;         // Present on 'failed' or 'in-progress'
}
```

### Behaviour Differences in Local Mode

| Operation | Remote mode | Local mode |
|-----------|-------------|------------|
| `start()` | Submits task to PS API | Runs agent-core loop in-process |
| `resume()` | Sends tool output to PS API | No-op (tools execute in-process) |
| `cancel()` | Calls PS API cancel endpoint | Calls `agent.abort()` on the in-process agent |
| `getEvents()` | Polls PS events API | Returns in-memory event log snapshot |

### Running the Local Agent Example

```bash
# Build first
npm run build

# Run the example
PS_API_TOKEN=<token> PS_API_URL=https://dev.lionis.ai npm run local-agent-test
```

See [`examples/local-agent-test.ts`](./examples/local-agent-test.ts) for a full working example with tools and event streaming.

---

## Agent Core Module

The `agent-core` module provides a low-level, event-driven agent runtime built on `@earendil-works/pi-ai`. This module is designed for advanced use cases where you need fine-grained control over agent execution, tool calling, and message handling.

> **Note**: The `agent-core` module adds additional dependencies including LLM provider SDKs. It's imported separately via `psadk/agent-core` to keep the main package lightweight.

### Installation

```typescript
import { Agent } from 'psadk/agent-core';
```

### Key Features

- **Stateful Agent Execution**: Lifecycle management with states (idle, running, stopped, error)
- **Event-Driven Architecture**: Hooks for messages, thinking, tool calls, and errors
- **Tool Execution**: Sequential and parallel tool calling modes
- **Message History**: Automatic tracking and state management
- **LLM Proxy**: Server-side routing for LLM calls
- **Type Safety**: Comprehensive TypeScript types with runtime validation using TypeBox

### Basic Example

```typescript
import { Agent } from 'psadk/agent-core';

const agent = new Agent({
  name: 'MyAgent',
  model: 'gpt-4',
  systemPrompt: 'You are a helpful assistant.',
  tools: [
    {
      name: 'get_weather',
      description: 'Get the current weather for a location',
      inputSchema: {
        type: 'object',
        properties: {
          location: { type: 'string' },
        },
        required: ['location'],
      },
      execute: async ({ location }) => {
        // Implementation
        return { temperature: 72, conditions: 'sunny' };
      },
    },
  ],
  onMessage: (msg) => console.log('Agent:', msg.content),
  onThinking: (thinking) => console.log('Thinking:', thinking),
  onToolCall: (toolCall) => console.log('Tool:', toolCall.name),
  onError: (error) => console.error('Error:', error),
});

// Start the agent and send a message
await agent.run("What's the weather in San Francisco?");
```

### Agent Configuration

```typescript
interface AgentConfig {
  name: string;
  model: string;
  systemPrompt?: string;
  tools?: Tool[];
  parallel?: boolean; // Enable parallel tool execution
  maxSteps?: number; // Maximum number of execution steps
  onMessage?: (message: Message) => void;
  onThinking?: (thinking: string) => void;
  onToolCall?: (toolCall: ToolCall) => void;
  onToolResult?: (result: ToolResult) => void;
  onError?: (error: Error) => void;
  onStateChange?: (state: AgentState) => void;
}
```

### Advanced: Using the Proxy

The proxy module allows you to route LLM calls through a server:

```typescript
import { ProxyStream } from 'psadk/agent-core';

const proxy = new ProxyStream({
  endpoint: 'https://your-server.com/llm',
  apiKey: 'your-api-key',
});

const stream = await proxy.createCompletion({
  model: 'gpt-4',
  messages: [{ role: 'user', content: 'Hello!' }],
});

for await (const chunk of stream) {
  console.log(chunk);
}
```

### Agent Lifecycle

The agent follows a state machine:

1. **idle**: Initial state, ready to accept tasks
2. **running**: Actively processing a task
3. **stopped**: Gracefully stopped
4. **error**: Encountered an error

```typescript
agent.onStateChange((state) => {
  console.log(`Agent state changed to: ${state}`);
});

// Stop the agent
await agent.stop();

// Reset to idle state
await agent.reset();
```

### Error Handling

```typescript
const agent = new Agent({
  name: 'SafeAgent',
  model: 'gpt-4',
  onError: (error) => {
    console.error('Agent error:', error.message);
    // Handle error (e.g., retry, alert, log)
  },
});

try {
  await agent.run('Process this task');
} catch (error) {
  // Errors are also thrown, so you can handle them here
  console.error('Task failed:', error);
}
```

## Developing with PSADK

### A2A Server

#### hello agent - simple chat agent

clone of https://github.com/a2aproject/a2a-samples/blob/main/samples/js/src/agents/coder/index.ts

```typescript
import { getServiceToken, PSAgent, PSAgentLogger } from 'psadk';
import dotenv from 'dotenv';

dotenv.config();

async function main() {
  const token = await getServiceToken({});
  const HelloAgent = new PSAgent({
    name: 'HelloAgent',
    description: 'A simple hello world agent',
    instruction: 'You are a friendly assistant that greets users.',
    token,
    port: 41241,
    log: new PSAgentLogger({ prefix: 'HelloAgent' }),
  });
  const { app } = HelloAgent.getA2AServer();
  app.listen(41241, () => {
    console.log('HelloAgent is running on http://localhost:41241');
  });
}

main().catch((error) => {
  console.error(error);
  process.exit(1);
});
```

create a .env file with the appropriate keys

```bash
PS_API_URL=https://dev.lionis.ai
PS_APP_KEY=sk_0abb4....

PS_CLIENT_ID=b5cc89b7-....
PS_PROJECT_ID=15a2e....
PS_WORKSPACE_ID=4ffba209-xxx-xxx-xxx-....

PS_SVC_CLIENT_ID=0483....
PS_SVC_CLIENT_SECRET=gfjt678...
PS_SVC_APP_KEY=sk_5f2967....
DEBUG=*

```

#### movie info agent - example with custom tools

clone of https://github.com/a2aproject/a2a-samples/blob/main/samples/js/src/agents/movie-agent/index.ts

```typescript
import { PSAI, PSAgent, PSAgentLogger } from 'psadk';
import dotenv from 'dotenv';
import { movie_search_tool, movie_searchpeople_tool } from './tmdb_tools.js';
import { prompt } from './movie_agent_prompts.js';
dotenv.config();

async function main() {
  // const token = await PSAI.getServiceToken({});
  const token = (await PSAI.getAuthToken()).accessToken;
  const instruction = prompt.replace('{{now}}', new Date().toLocaleString());
  const MovieAgent = new PSAgent({
    name: 'MovieAgent',
    description: 'An agent that can answer questions about movies using TMDB.',
    instruction,
    model: 'gpt-4.1-mini',
    token,
    port: 41241,
    log: new PSAgentLogger({ prefix: 'MovieAgent' }),
    tools: [movie_search_tool, movie_searchpeople_tool],
  });
  const { app, port } = MovieAgent.getA2AServer();
  app.listen(port, () => {
    console.log(`MovieAgent is running on http://localhost:${port}`);
  });
}

main().catch((error) => {
  console.error(error);
  process.exit(1);
});
```

Use can use [a2a inspector ui](https://a2a-inspector-908687846511.us-central1.run.app/)
just enter the agent card url http://localhost:41241/.well-known/agent-card.json to interact with the agent

#### Custom tool

```typescript
import { PSAgentTool } from 'psadk';
import { z } from 'zod/v4';

export const movie_search_tool = new PSAgentTool({
  name: 'search_movies',
  description: 'search TMDB for movies by title',
  type: 'API',
  local_execution: true,
  hitl: true,
  inputschema: {
    title: `search_movies_inputs.`,
    description: 'Inputs for search_movies tool.',
    url: 'http://localhost/search_movies',
    method: 'POST',
    requestBody: z.toJSONSchema(
      z
        .object({
          query: z.string().meta({
            title: 'query',
            description: 'The search query string for movies.',
          }),
        })
        .meta({
          title: 'search_movies_request_body',
          description:
            'The request body parameters for the search_movies tool.',
        }),
      { io: 'input' },
    ),
  },
  runCallback: async ({ query }) => {
    console.log('[tmdb:searchMovies]', JSON.stringify(query));
    try {
      const data = await callTmdbApi('movie', query);

      // Only modify image paths to be full URLs
      const results = data.results.map((movie: any) => {
        if (movie.poster_path) {
          movie.poster_path = `https://image.tmdb.org/t/p/w500${movie.poster_path}`;
        }
        if (movie.backdrop_path) {
          movie.backdrop_path = `https://image.tmdb.org/t/p/w500${movie.backdrop_path}`;
        }
        return movie;
      });

      return {
        status: 'success',
        output: {
          ...data,
          results,
        },
      };
    } catch (error) {
      console.error('Error searching movies:', error);
      // Re-throwing allows the caller to handle it appropriately
      throw error;
    }
  },
});
```

#### local agent - run agent in-process with event streaming

```typescript
import { PSAgent, PSAgentLogger } from 'psadk';
import type { LocalAgentEvent } from 'psadk';
import dotenv from 'dotenv';

dotenv.config();

async function main() {
  const token = process.env.PS_API_TOKEN!;
  const baseUrl = process.env.PS_API_URL || 'https://dev.lionis.ai';

  const agent = new PSAgent(
    {
      name: 'LocalHelloAgent',
      description: 'A simple agent running in local mode',
      instruction: 'You are a friendly assistant that greets users.',
      model: 'gpt-4o-mini',
      token,
      local: true,                         // run agent-core in-process
      llm_config: {
        reasoning_effort: 'high',          // 'medium'|'high' → reasoning: true; 'low'|'minimal' → reasoning: false
        context_window: 128000,            // token context window (default: 128000)
        max_completion_tokens: 4096,       // max output tokens (default: 4096)
      },
      onEvent: (event: LocalAgentEvent) => {
        switch (event.agentStatus) {
          case 'pending':
            console.log('⏳ Agent starting...');
            break;
          case 'in-progress':
            console.log('🔄 Agent thinking...');
            break;
          case 'waiting':
            console.log(`🔧 Calling tool: ${event.tool}`);
            console.log('   Input:', JSON.stringify(event.toolInput?.request_body));
            break;
          case 'completed':
            console.log('✅ Done:', event.output?.response);
            break;
          case 'failed':
            console.error('❌ Failed:', event.message);
            break;
        }
      },
    },
    new PSAgentLogger({ prefix: 'LocalHelloAgent' }),
    {
      agents: baseUrl,
      events: baseUrl,
      llm: baseUrl,                        // LLM calls route through PS LLM API
    },
  );

  const result = await agent.start({ query: 'Say hello!' });
  console.log('Result:', result);
}

main().catch((error) => {
  console.error(error);
  process.exit(1);
});
```

### Client: Sending a Message

The A2AClient makes it easy to communicate with any A2A-compliant agent.

```typescript
// client.ts
import { A2AClient, SendMessageSuccessResponse } from '@a2a-js/sdk/client';
import { Message, MessageSendParams } from '@a2a-js/sdk';
import { v4 as uuidv4 } from 'uuid';

async function run() {
  // Create a client pointing to the agent's Agent Card URL.
  const client = await A2AClient.fromCardUrl(
    'http://localhost:41241/.well-known/agent-card.json',
  );

  const sendParams: MessageSendParams = {
    message: {
      messageId: uuidv4(),
      role: 'user',
      parts: [{ kind: 'text', text: 'Hi there!' }],
      kind: 'message',
    },
  };

  const response = await client.sendMessage(sendParams);

  if ('error' in response) {
    console.error('Error:', response.error.message);
  } else {
    const result = (response as SendMessageSuccessResponse).result as Message;
    console.log('Agent response:', result.parts[0].text); // "Hello, world!"
  }
}

await run();
```
