Skip to main content

Using Penguin's API

This guide provides examples and code snippets for interacting with Penguin's API endpoints in various programming languages and environments.

Overview

Penguin provides a comprehensive REST API and WebSocket interface for integrating with applications, scripts, and other services. The API allows you to:

  • Send messages and receive responses
  • Stream responses in real-time
  • Manage conversations
  • Persist and control session-scoped goals
  • Execute tasks
  • Access context files
  • Monitor token usage

Quick Start

The examples below assume a local server on the default 127.0.0.1:9000. If web auth is enabled, include an X-API-Key header using a key from PENGUIN_API_KEYS or the startup token printed by penguin-web.

Basic Chat Request

import requests

# Send a simple message
response = requests.post(
"http://127.0.0.1:9000/api/v1/chat/message",
headers={"X-API-Key": "your-key"},
json={
"text": "Write a Python function to calculate the Fibonacci sequence."
}
)

# Print the response
print(response.json()["response"])

JavaScript Example

// Send a message using fetch
async function sendMessage(text) {
const response = await fetch('http://127.0.0.1:9000/api/v1/chat/message', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-Key': 'your-key'
},
body: JSON.stringify({
text: text
})
});

const data = await response.json();
return data.response;
}

// Usage
sendMessage("Explain quantum computing in simple terms")
.then(response => console.log(response))
.catch(error => console.error('Error:', error));

Chat API

Regular Chat

To send a message and receive a response:

curl -X POST http://127.0.0.1:9000/api/v1/chat/message \
-H "Content-Type: application/json" \
-d '{"text": "Generate a hello world program in Rust"}'

With Conversation Context

To continue an existing conversation:

curl -X POST http://127.0.0.1:9000/api/v1/chat/message \
-H "Content-Type: application/json" \
-d '{
"text": "Explain the key components of the code you just generated",
"conversation_id": "conversation_20240330_123456"
}'

With Session-Scoped Directory

Use session_id/conversation_id plus directory to bind a session to a specific repo root.

curl -X POST http://127.0.0.1:9000/api/v1/chat/message \
-H "Content-Type: application/json" \
-d '{
"text": "Run checks in this repo",
"session_id": "repo-session-1",
"directory": "/absolute/path/to/repo"
}'

Notes:

  • The first valid directory bound to a session becomes the session root.
  • Rebinding that same session to a different directory returns 409.
  • Invalid directories return 400.

Tool Use Through Chat

Tools are invoked by the model during chat execution, not by calling tool names directly from the web API. To let Penguin inspect or modify a repo, provide a stable session_id and directory; the runtime binds that session to the repo root and executes native provider tool calls there.

Tool-related behavior to expect:

  • successful tool runs appear in the REST action_results payload
  • live clients receive message/tool-part events for tool start and completion
  • approval-gated tools pause until approved or denied
  • native provider tool calls are preferred; ActionXML is retained only as a fallback compatibility path
  • Penguin avoids executing both native tool calls and duplicate ActionXML from the same assistant turn

Streaming Responses

For real-time streaming of responses, use the WebSocket API:

// Create WebSocket connection
const socket = new WebSocket('ws://127.0.0.1:9000/api/v1/chat/stream');

// Connection opened
socket.addEventListener('open', (event) => {
socket.send(JSON.stringify({
text: "Write a step-by-step guide to installing TensorFlow.",
session_id: "stream-session-1",
directory: "/absolute/path/to/repo"
}));
});

// Listen for messages
socket.addEventListener('message', (event) => {
const data = JSON.parse(event.data);

if (data.event === 'start') {
console.log('Response started');
} else if (data.event === 'token') {
process.stdout.write(data.data.token);
} else if (data.event === 'complete') {
console.log('\nResponse completed');
console.log('Action results:', data.data.action_results);
}
});

Conversation Management

List Conversations

import requests

# Get all conversations
response = requests.get("http://127.0.0.1:9000/api/v1/conversations")
conversations = response.json()["conversations"]

for conv in conversations:
print(f"ID: {conv['session_id']}, Messages: {conv['message_count']}")

Create New Conversation

import requests

# Create a new conversation
response = requests.post("http://127.0.0.1:9000/api/v1/conversations/create")
conversation_id = response.json()["conversation_id"]

print(f"Created conversation: {conversation_id}")

Retrieve Conversation

import requests

# Get a specific conversation
conversation_id = "conversation_20240330_123456"
response = requests.get(f"http://127.0.0.1:9000/api/v1/conversations/{conversation_id}")
data = response.json()

# Get messages
for message in data["messages"]:
print(f"{message['role']}: {message['content']}")

Session Goals

A session goal is durable lifecycle state attached to an existing saved session. The HTTP API separates persistence from execution: creating a goal stores it, while POST .../goal/run starts RunMode execution. The TUI's /goal <objective> command calls both operations for convenience.

There is one goal per session. /247 is a TUI alias for /goal; it does not create a second goal type and does not enable automatic continuation.

The current implementation assumes one server process owns a session store. Its locks and live-run registry are process-local, so do not point multiple web workers at the same conversation files. Cross-process leases are not part of this release.

Read Goal State

curl http://127.0.0.1:9000/api/v1/session/SESSION_ID/goal \
-H "X-API-Key: your-key"

An existing session with no goal returns 200:

{"goal": null, "status": "ok"}

Create or Replace a Goal

curl -X POST http://127.0.0.1:9000/api/v1/session/SESSION_ID/goal \
-H "Content-Type: application/json" \
-H "X-API-Key: your-key" \
-d '{
"objective": "Ship the session-goal API with regression tests",
"token_budget": 50000,
"replace": false
}'

The objective must be non-empty. token_budget, when present, is a positive cumulative stop threshold across runs. Replacing an unfinished goal requires "replace": true; completed goals may be replaced without the flag. A goal with an active run cannot be replaced until that run is stopped and released.

Pause and Resume

Pause through the goal mutation endpoint:

curl -X POST http://127.0.0.1:9000/api/v1/session/SESSION_ID/goal \
-H "Content-Type: application/json" \
-H "X-API-Key: your-key" \
-d '{"status": "paused"}'

Resume by setting active, then explicitly starting a run:

curl -X POST http://127.0.0.1:9000/api/v1/session/SESSION_ID/goal \
-H "Content-Type: application/json" \
-H "X-API-Key: your-key" \
-d '{"status": "active"}'

curl -X POST http://127.0.0.1:9000/api/v1/session/SESSION_ID/goal/run \
-H "Content-Type: application/json" \
-H "X-API-Key: your-key" \
-d '{"max_iterations": 8, "timeout_seconds": 600}'

max_iterations and timeout_seconds are optional positive integers with no Penguin hard maximum. If omitted, they remain unset and do not stop execution. Likewise, the runtime passes a token ceiling only when the goal has an explicit cumulative token_budget. Because usage is reported after a provider response, the last billed turn may cross that configured threshold; a valid structured finish on that turn still wins. An exhausted explicit cumulative budget returns budget_limited.

Goal creation and execution are separate HTTP requests. If a client loses its connection after creation but before /goal/run is acknowledged, read the persisted goal first: retry the same create request identifiers when available, or call /goal/run for the already-active, unrun goal. Do not replace the goal just to recover from an uncertain response.

Pausing does not silently rewrite a completed result from an already-running request. Goal ID, run ID, revision, and active status fence late completion so paused, cleared, or replaced state wins.

Clear a Goal

curl -X DELETE http://127.0.0.1:9000/api/v1/session/SESSION_ID/goal \
-H "X-API-Key: your-key"

Successful reads and mutations use an operation acknowledgement at the top level; the lifecycle value remains inside goal:

{
"status": "ok",
"goal": {
"id": "goal_...",
"objective": "Ship the session-goal API with regression tests",
"status": "active",
"revision": 3,
"token_budget": 50000,
"tokens_used": 12000,
"time_used_seconds": 91.4,
"active_run_id": null
}
}

Successful /goal/run responses instead expose the resulting lifecycle status at the top level and include the bounded RunMode result:

{
"status": "active",
"goal": { "id": "goal_...", "status": "active" },
"result": {
"status": "pending_review",
"finish_status": "partial",
"finish_summary": "Implemented the first verified slice"
}
}

Goal lifecycle values are active, paused, blocked, usage_limited, budget_limited, and complete. A successful validated finish_task(status="done") marks a goal complete; the deprecated task_completed result marker is also honored when it carries an anchored machine-readable done status. Partial progress remains active; clarification or an explicit blocked finish leaves it blocked.

Goal API errors use these status codes:

  • 404: session missing, or a pause/resume/run/clear operation has no goal.
  • 409: unfinished replacement lacks replace=true, a run/session is busy, or the requested lifecycle transition conflicts with current state.
  • 422: malformed JSON fields, unknown status, empty objective, non-positive or over-limit budget/iteration value, or other schema validation failure.
  • 500: the server could not durably save/finalize goal state or initialize the scoped runtime; the run is fenced for recovery rather than reported as success.

Project and Task Management

Create Project

import requests

# Create a new project
response = requests.post(
"http://127.0.0.1:9000/api/v1/projects",
json={
"name": "Website Development",
"description": "Create a responsive website with React and FastAPI backend"
}
)

project_data = response.json()
print(f"Created project: {project_data}")

Execute Task

import requests

# Run a task
response = requests.post(
"http://127.0.0.1:9000/api/v1/tasks/execute",
json={
"name": "Generate API documentation",
"description": "Create OpenAPI documentation for all endpoints",
"continuous": False,
"time_limit": 30 # minutes
}
)

print(f"Task execution started: {response.json()}")

Utility Endpoints

Token Usage

import requests

# Get runtime/global token usage stats
response = requests.get("http://127.0.0.1:9000/api/v1/token-usage")
usage = response.json()["usage"]
print(f"Usage scope: {usage['scope']}")

# Get transcript-safe, session-scoped context-window telemetry
response = requests.get(
"http://127.0.0.1:9000/api/v1/token-usage",
params={"session_id": "example-session"},
)
usage = response.json()["usage"]

if usage["scope"] == "session":
print(f"Session context usage: {usage['current_total_tokens']} tokens")

Context Files

import requests

# List available context files
response = requests.get("http://127.0.0.1:9000/api/v1/context-files")
files = response.json()["files"]

for file in files:
print(f"File: {file['path']}, Size: {file['size']}")

# Load a context file
response = requests.post(
"http://127.0.0.1:9000/api/v1/context-files/load",
json={
"file_path": "docs/api_reference.md"
}
)

print(f"Context file loaded: {response.json()}")

Error Handling

The API uses standard HTTP status codes:

  • 200: Success
  • 400: Bad Request (invalid parameters)
  • 404: Not Found (resource doesn't exist)
  • 409: Conflict (resource lifecycle or active-run conflict)
  • 422: Unprocessable Entity (request schema or value validation failed)
  • 500: Server Error

Example error handling:

import requests

try:
response = requests.post(
"http://127.0.0.1:9000/api/v1/chat/message",
json={"invalid_param": "value"}
)
response.raise_for_status()

data = response.json()
print(data["response"])

except requests.exceptions.HTTPError as e:
if response.status_code == 400:
print(f"Bad request: {response.json().get('detail')}")
elif response.status_code == 500:
print(f"Server error: {response.json().get('detail')}")
else:
print(f"HTTP error: {e}")

except Exception as e:
print(f"Error: {e}")

Integrating with Other Languages

cURL

# Send a message
curl -X POST http://127.0.0.1:9000/api/v1/chat/message \
-H "Content-Type: application/json" \
-d '{"text": "What are the key features of Penguin?"}'

Java

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

public class PenguinApiExample {
public static void main(String[] args) {
try {
HttpClient client = HttpClient.newHttpClient();

String requestBody = "{\"text\": \"Explain SOLID principles\"}";

HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("http://127.0.0.1:9000/api/v1/chat/message"))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(requestBody))
.build();

HttpResponse<String> response = client.send(request,
HttpResponse.BodyHandlers.ofString());

System.out.println(response.body());
} catch (Exception e) {
e.printStackTrace();
}
}
}

Go

package main

import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
)

func main() {
// Create request body
requestBody, _ := json.Marshal(map[string]string{
"text": "Explain container orchestration",
})

// Send POST request
resp, err := http.Post(
"http://127.0.0.1:9000/api/v1/chat/message",
"application/json",
bytes.NewBuffer(requestBody),
)

if err != nil {
fmt.Println("Error:", err)
return
}
defer resp.Body.Close()

// Read response
body, _ := ioutil.ReadAll(resp.Body)

// Parse JSON response
var result map[string]interface{}
json.Unmarshal(body, &result)

fmt.Println(result["response"])
}

Rate Limiting

The API currently does not implement rate limiting, but best practices include:

  • Limit request frequency to avoid overloading the server
  • Implement retries with exponential backoff for failed requests
  • Use a single conversation for related messages to maintain context

Security Considerations

When deploying Penguin in a production environment:

  • Use HTTPS for all API communication
  • Implement proper authentication and authorization
  • Restrict CORS settings to known domains
  • Review and limit model capabilities as needed

Next Steps