PersonalityCore is the Solar Network agent runtime service.
It provides:
- config-driven agent definitions
- REST APIs under
/api - SSE streaming for chat runs
- Postgres-backed conversation, message, and run persistence
- gRPC APIs for internal service-to-service usage
The current implementation is backend-only. Agents are defined on the server side through TOML, not created by clients.
Each agent can define:
idnamedescriptionsystemPromptsystemPromptFilemodel- model defaults such as
temperature,topP, andmaxCompletionTokens - optional chat-specific output cap via
chatMaxCompletionTokens abilitiesenabled
At runtime:
- clients list available agents
- clients create conversations bound to an
agent_id - clients send messages and trigger runs
- runs can be non-streaming JSON or streaming SSE
Abilities are part of the agent definition.
Current humanization-related abilities:
humanizer: composite ability that enables all humanization features belowmemory: passive fact extraction and long-term remembered factssaved_memory: agent-owned deliberately saved memoriescross_conversation_memory: recall from other recent conversations for the same user account + agentmood: rolling emotional tonerelationship: familiarity and relationship posture
Chat integration ability:
chat: enables Solar Network bot messaging through a configured bot account and keeps one websocket connection open per enabled integrated agent
These are server-side systems. They do not require client-side function-calling support. For humanization, the current server behavior includes:
- passive fact extraction from user messages
- cross-conversation recall from other recent threads for the same user account + agent
- a distinct agent-owned saved-memory bucket for messages like
remember that ...,please remember ..., ordon't forget ... - a separate agent-global self-note bucket keyed only by
agent_id, shared across every conversation that uses the same agent
For Solar chat, humanizer state is keyed by the inbound sender's account_id, not the per-room synthetic conversation account. That lets impressions and memory carry across rooms and the direct run API when the same user account is involved.
The saved-memory bucket is meant to represent deliberate agentic memory, even though the current implementation still uses server-side heuristics until explicit tool-calling is added. Agent-global self notes are different: they represent the agent's own stable identity, preferences, lore, and ongoing projects. Those notes are injected into the system prompt for every run of that agent.
- cmd/main.go: process entrypoint
- internal/app/app.go: runtime wiring
- internal/config/config.go: TOML config loading
- internal/service/conversation.go: conversation and run lifecycle
- internal/handler/routes.go: REST and SSE handlers
- internal/grpcsvc/personality.go: gRPC service
- Go
1.26.3 - PostgreSQL
- an OpenAI-compatible chat endpoint
Optional:
- Solar auth gRPC service via
auth.target
Start from config.example.toml.
Important sections:
database.dsnhttp.portgrpc.portauth.targetsolarNetwork.baseUrlprovidersDirprovidersagents.itemsagents.dir
Models are referenced by agents in <provider>/<model> form, for example:
openai/gpt-4.1-miniopenrouter/baai/bge-m3
You can define agents in two ways:
- Inline in the main config:
[[agents.items]]
id = "support"
name = "Support"
description = "General support assistant"
systemPrompt = "You are the Solar Network support assistant."
model = "openai/gpt-4.1-mini"
abilities = []
enabled = trueIf you want the prompt in a separate file:
[[agents.items]]
id = "support"
name = "Support"
description = "General support assistant"
systemPromptFile = "./prompts/support.md"
model = "openai/gpt-4.1-mini"
abilities = []
enabled = true- Split across multiple TOML files in
agents.dir:
[agents]
dir = "./agents.d"Example extra file:
[agents]
[[agents.items]]
id = "writer"
name = "Writer"
description = "Writing assistant"
systemPromptFile = "../prompts/writer.md"
model = "openai/gpt-4.1-mini"
abilities = []
enabled = trueThe service merges inline agents and agents.dir/*.toml at startup.
systemPromptFile is resolved relative to the config file that declares the agent, so split agent files can safely point at nearby prompt files.
Agents with abilities = ["chat"] also require a Solar bot integration block:
[solarNetwork]
baseUrl = "https://api.dyson.example"
[[agents.items]]
id = "support-bot"
name = "Support Bot"
description = "Replies in Solar chat as a bot"
systemPrompt = "You are the Solar support bot."
model = "openai/gpt-4.1-mini"
abilities = ["chat"]
enabled = true
[agents.items.solar-network-integration]
accountName = "support-bot"
accessToken = "..."The integration block is server-only: public HTTP and gRPC agent metadata expose abilities, but never return the bot credentials.
Each enabled integrated agent maintains one websocket connection to {solarNetwork.baseUrl}/ws.
When a chat-linked Solar conversation is active, outbound remote messages should be sent by send_chat_message or send_chat_message_batch; NO_REPLY is the explicit silence token, and plain assistant text is forwarded as a fallback when the model skips tool calling.
Chat tool-calling also exposes list_self_notes, save_self_note, and delete_self_note so an agent can inspect and update its own persistent identity notes shared across all conversations.
Inbound Solar chat image attachments are passed to the model as multimodal image inputs using {solarNetwork.baseUrl}/drive/files/{file_id}.
When the agent replies in plain assistant text for a Solar chat conversation, each non-empty newline-delimited line is sent as a separate outbound chat message. In streaming mode, completed lines are sent immediately when the newline arrives.
For live inbound handling, a direct mention or reply to the bot opens a 5-minute active follow-up window so the bot can continue the current group-chat exchange.
Every run also appends explicit message timestamps in context and a final Current date and time: system message so the model can reason about chronology without depending on cache-retained earlier prompt sections.
When a thread grows beyond the live history window, older messages are automatically compacted into a persisted thread summary that is injected back into future runs as Earlier compacted thread context:.
Agents can also opt into autonomous:
[[agents.items]]
id = "support-bot"
name = "Support Bot"
description = "Replies in Solar chat and can proactively follow up"
systemPrompt = "You are the Solar support bot."
model = "openai/gpt-4.1-mini"
chatMaxCompletionTokens = 160
abilities = ["chat", "autonomous"]
enabled = true
[agents.items.solar-network-integration]
accountName = "support-bot"
accessToken = "..."
[agents.items.autonomous]
wakeInterval = "10m"
wakePrompt = "Check active rooms and DMs for proactive follow-up opportunities."autonomous enables server-side agent-initiated runs. The current implementation supports:
- manual wake triggers through
POST /api/agents/:id/autonomous-runs - periodic wake-ups for agents with
autonomous.wakeIntervalset - synthetic autonomous wake request messages persisted with
role = "system"and metadatasource = "autonomous" - trusted outbound conversation starts through
POST /api/internal/agents/:id/start-conversationwith headerX-Autonomous-Secret
If chat replies are too verbose, set a lower chatMaxCompletionTokens on that agent. This overrides maxCompletionTokens only for Solar/chat-style execution paths and leaves ordinary non-chat runs unchanged.
Current boundaries:
- periodic wakes currently target existing Solar-bound conversations only
- DM bindings are always eligible for periodic wakes
- periodic pickup of old group-chat messages is suppressed unless the latest inbound group message directly mentioned or replied to the bot
- if you want proactive Solar outreach, combine
autonomouswithchat - lookup-only tool calls no longer terminate the Solar tool loop early; the model can inspect posts or profiles first, then decide whether to send a message
Example trusted start request:
curl -X POST http://127.0.0.1:8090/api/internal/agents/support-bot/start-conversation \
-H 'Content-Type: application/json' \
-H 'X-Autonomous-Secret: YOUR_SECRET' \
-d '{"target_account_name":"alice","prompt":"Say hi and ask how her project is going."}'If you already know the Solar account ID, you can also send target_account_id directly.
The TUI binary can call the same endpoint in one-shot mode:
go run ./cmd/tui \
-base-url http://127.0.0.1:8090 \
-agent-id support-bot \
-autonomous-secret YOUR_SECRET \
-start-user alice \
-start-prompt "Say hi and ask how her project is going."Providers can also be defined in two ways.
- Inline in the main config:
providersDir = "./models.d"
[[providers]]
id = "openai"
type = "openai"
apiKey = "..."
baseUrl = ""
timeout = "90s"
maxCompletionTokens = 2048
temperature = 0.7
topP = 1.0- Split across multiple files in
providersDir, for example./models.d/openrouter.toml:
[[providers]]
id = "openrouter"
type = "openai-compatible"
apiKey = "..."
baseUrl = "https://openrouter.ai/api/v1"
timeout = "90s"
maxCompletionTokens = 2048
temperature = 0.7
topP = 1.0The service merges inline providers and providersDir/*.toml at startup.
Example separated layout:
config.toml
agents.d/
support.toml
writer.toml
models.d/
openai.toml
openrouter.toml
prompts/
support.md
writer.md
Example config.toml:
providersDir = "./models.d"
[agents]
dir = "./agents.d"
[auth]
offline = true
offlineAccountId = "local-dev"
autonomousSecret = ""Example agents.d/support.toml:
[agents]
[[agents.items]]
id = "support"
name = "Support"
description = "General support assistant"
systemPromptFile = "../prompts/support.md"
model = "openai/gpt-4.1-mini"
abilities = []
enabled = trueExample with humanization scopes enabled:
[agents]
[[agents.items]]
id = "michan"
name = "Michan"
description = "A more person-like companion agent"
systemPromptFile = "../prompts/michan.md"
model = "deepseek/deepseek-v4-flash"
abilities = ["humanizer"]
enabled = trueExample agents.d/writer.toml:
[agents]
[[agents.items]]
id = "writer"
name = "Writer"
description = "Writing assistant"
systemPromptFile = "../prompts/writer.md"
model = "openai/gpt-4.1-mini"
abilities = []
enabled = trueExample models.d/openai.toml:
[[providers]]
id = "openai"
type = "openai"
apiKey = "YOUR_OPENAI_KEY"
baseUrl = ""
timeout = "90s"
maxCompletionTokens = 2048
temperature = 0.7
topP = 1.0Example models.d/openrouter.toml:
[[providers]]
id = "openrouter"
type = "openai-compatible"
apiKey = "YOUR_OPENROUTER_KEY"
baseUrl = "https://openrouter.ai/api/v1"
timeout = "90s"
maxCompletionTokens = 2048
temperature = 0.7
topP = 1.0Example prompts/support.md:
You are the Solar Network support assistant.
Answer clearly and keep replies operational.Startup fails when:
- no enabled agents exist
- an agent id is duplicated
- required fields like
idornameare missing - no providers exist
- a provider id is duplicated
HTTP routes require an account identity.
Supported modes:
- offline mode: set
auth.offline = trueto skip Solar auth entirely for local testing - production mode: configure
auth.targetto use Solar auth viasrc.solsynth.dev/sosys/go/pkg/auth - local/dev mode: if
auth.allowDevIds = true, sendX-Account-Id: your-account-id
Offline mode behavior:
- no auth token is required
- every request uses the same
auth.offlineAccountId - this is meant to simulate one fixed local user across the whole service instance
Example offline config:
[auth]
offline = true
offlineAccountId = "local-dev"Solar Network note:
- Solar gRPC commonly uses self-signed TLS certificates
- when dialing Solar auth over TLS, set
auth.useTLS = trueandauth.tlsSkipVerify = true
Example Solar auth config:
[auth]
target = "grpcs://padlock:7003"
useTLS = true
tlsSkipVerify = true
offline = falseExample dev request header:
X-Account-Id: user-123go run ./cmd --config ./config.tomlUseful flags:
--config ./config.toml--pretty
Useful environment variables:
CONFIG_PATHZEROLOG_PRETTY=trueLOG_LEVEL=debugDATABASE_DSN
Generation logging:
LOG_LEVEL=info: run creation, generation start, completion, and failureLOG_LEVEL=debug: model preparation, humanizer overlay injection, model invocation, and stream chunk summary
Example:
LOG_LEVEL=debug go run ./cmd --config ./config.toml --prettyFor fully local testing, a common setup is:
[auth]
offline = true
providersDir = "./models.d"
[[providers]]
id = "openai"
type = "openai"
apiKey = "..."
[[agents.items]]
id = "support"
name = "Support"
systemPrompt = "You are a helpful assistant."
model = "openai/gpt-4.1-mini"There is also a minimal terminal client for local testing.
It is designed for the offline mock-user mode:
[auth]
offline = true
offlineAccountId = "local-dev"Start the server:
go run ./cmd --config ./config.tomlThen open the TUI in another terminal:
go run ./cmd/tui --base-url http://127.0.0.1:8090Useful flags:
--base-url http://127.0.0.1:8090--agent-id support--stream=true--account-id user-123
The --account-id flag is only useful when you are not using offline mode and want to send X-Account-Id in local dev mode.
Controls:
Enter: send the current messageCtrl+N: create a new conversation for the current agentTab/Shift+Tab: switch agents and start a fresh conversationCtrl+S: toggle streaming SSE vs non-streaming JSON runsQ: quit
The TUI will:
- load enabled agents from
/api/agents - create a conversation automatically on startup
- append replies live when streaming is enabled
All endpoints live under /api.
Account holders can mint limited AI-only credentials without sharing a Solar Network access token:
POST /api/openai/credentialscreates asat_...token and returns it exactly once.GET /api/openai/credentialslists credential metadata and cumulative usage.DELETE /api/openai/credentials/:idpermanently revokes a credential.
Creation accepts name, optional agent_ids, providers, and models allowlists, plus usage_limit and usage_currency. An empty allowlist dimension is unrestricted. Models use provider/model. Credentials use Authorization: Bearer sat_... only with /v1/chat/completions or /api/v1/chat/completions; server-owned tools are disabled for credential calls. Usage is priced from the configured model pricing, accumulated per credential, and rejected once the limit is exhausted.
GET /api/agentsGET /api/agents/:id
Example:
curl http://localhost:8090/api/agents \
-H 'X-Account-Id: user-123'POST /api/conversationsGET /api/conversations?take=20&offset=0GET /api/conversations/:idGET /api/conversations/:id/messages?take=20&offset=0POST /api/conversations/:id/messagesPOST /api/conversations/:id/runsGET /api/conversations/:id/runs?take=20&offset=0GET /api/conversations/:id/runs/:runId
List endpoints follow Solar pagination style:
- request:
take,offset - response header:
X-Total
Optional Wallet-backed billing is configured with [billing]. Every configured
model may define a [providers.models.pricing] section with currency,
input, and output per 1M tokens; for example, a model may charge in
golds and another in points. A model without that section is free. An agent
may set billingMultiplier to adjust that model price for calls made through
the agent. Usage limits still count free-model calls, while a blacklisted
account cannot use any Personality model.
Priced models additionally require a Wallet payment wallet; this lookup is
cached per account for 10 minutes. Free models do not require one.
Wallet charges are truncated to two decimal places; an unbillable fractional
remainder stays outstanding and rolls into the next settlement.
Set billing.serviceFeePercentage to add a global percentage to every priced
model call (for example, "5" adds a 5% fee).
Billing usage is settled at UTC midnight (with startup catch-up). Configure
instantBillingWall to charge an account as soon as its unpaid gold balance
reaches that amount. A failed Wallet transaction blacklists the account.
billing.payeeAccountId is optional; when omitted, the Wallet service receives
a null payee_account_id and chooses its own default/system payee.
Billing administrators must be superusers or hold the Padlock permission
personality.billing.manage and can manage overrides at:
GET /api/admin/billing/accounts/:accountIdPUT /api/admin/billing/accounts/:accountIdPOST /api/admin/billing/accounts/:accountId/unblacklist
Each user can view their policy at GET /api/billing/me and choose their own
immediate-settlement spending quota with PUT /api/billing/me/spending-quota.
This does not grant access to change administrator-set limits or blacklist
status.
curl -X POST http://localhost:8090/api/conversations \
-H 'Content-Type: application/json' \
-H 'X-Account-Id: user-123' \
-d '{
"agent_id": "support",
"title": "Support session"
}'curl -X POST http://localhost:8090/api/conversations/CONVERSATION_ID/messages \
-H 'Content-Type: application/json' \
-H 'X-Account-Id: user-123' \
-d '{
"content": "I need help with my account"
}'curl -X POST http://localhost:8090/api/conversations/CONVERSATION_ID/runs \
-H 'Content-Type: application/json' \
-H 'X-Account-Id: user-123' \
-d '{
"message": "Summarize the issue and suggest next steps",
"stream": false
}'POST /api/conversations/:id/runs also accepts input_parts for multimodal user input.
Use message for the main text prompt, then append image parts by URL or base64.
Models declare which modalities they support via modalities in their provider model config. When a model supports image, image parts are sent directly. When it does not, PersonalityCore automatically summarizes images using the app-wide visionModel and injects the summary as text. Summaries are cached in the database.
curl -X POST http://localhost:8090/api/conversations/CONVERSATION_ID/runs \
-H 'Content-Type: application/json' \
-H 'X-Account-Id: user-123' \
-d '{
"message": "What is happening in this image?",
"input_parts": [
{
"type": "image_url",
"image_url": "https://example.com/photo.jpg",
"detail": "high"
}
],
"stream": false
}'Base64 uploads are also supported:
{
"message": "Read the chart in this screenshot",
"input_parts": [
{
"type": "image_url",
"image_base64": "iVBORw0KGgoAAAANSUhEUgAA...",
"mime_type": "image/png",
"detail": "low"
}
]
}Example provider config with per-model modalities and embedding model typing:
[[providers]]
id = "openai"
type = "openai"
apiKey = "..."
[[providers.models]]
name = "gpt-4o"
modalities = ["image", "audio", "video"]
[[providers.models]]
name = "gpt-4.1-mini"
modalities = ["image"]
[[providers.models]]
name = "gpt-3.5-turbo"
# no modalities — treated as text-only, images summarized via visionModel
[[providers.models]]
name = "text-embedding-3-small"
type = "embedding"
# embedding models are reserved for embedding RPCsTo enable image summarization for non-vision models, set visionModel under [personality]:
[personality]
visionModel = "openai/gpt-4.1-mini"
defaultEmbeddingModel = "openai/text-embedding-3-small"If no visionModel is configured, image parts for non-vision models are replaced with a placeholder.
curl -N -X POST http://localhost:8090/api/conversations/CONVERSATION_ID/runs \
-H 'Content-Type: application/json' \
-H 'X-Account-Id: user-123' \
-d '{
"message": "Explain this step by step",
"stream": true
}'SSE events currently emitted:
run.startedmessage.deltamessage.completedrun.completedrun.failedheartbeat
The shared protobuf contract lives in:
Generated Go bindings live in:
Implemented personality RPCs:
ListAgentsGetAgentRunConversationComplete
Implemented embedding RPCs:
GenerateEmbeddingGenerateEmbeddings
Embedding RPC behavior:
- single-text and batch generation are both supported
- set the default model with
personality.defaultEmbeddingModel - override per-call settings with gRPC metadata headers
x-embedding-modelandx-embedding-dimensions - only provider models marked with
type = "embedding"are allowed for embedding calls; all others are treated as completion/chat models
RunConversation behavior:
- if
conversation_idis empty, the service creates a new conversation usingagent_id - if
conversation_idis present, the run continues that conversation - current gRPC execution is unary only
The service persists:
- conversation threads
- conversation messages
- conversation runs
Threads are owned by account_id, and every access is scoped to that owner.
Current behavior:
- a conversation is permanently bound to one
agent_id - user and assistant messages are stored
- the final run result is stored
- token-by-token chunks are streamed live but not individually persisted
- The current model adapter uses
github.com/cloudwego/eino-ext/components/model/openai. - Multiple providers are now resolved from
[[providers]]plusprovidersDir/*.toml. - Provider type support is currently implemented for OpenAI-compatible backends via
type = "openai"ortype = "openai-compatible". abilitiesare the primary agent capability field.abilitiesis the canonical agent capability field.autonomousis an initiation ability: it lets the server wake an agent without a fresh user message.- The gRPC service is intended for internal Solar usage; the primary client API is REST + SSE.
Current verification command:
go test ./...