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​