A multi-role AI agent "software company" simulator written in Java (Maven, JUnit 5).
A team of LLM-powered employees β CEO, COO, HR, team leads, developers, testers, security
engineers, β¦ β runs like a real company: it works on a corporate shift clock, reacts to
events instead of spinning in a while(true) loop, and every employee owns a personal
computer of its own where its files, notes, tools and MCP servers live.
This design solves the classic problems of long-running agent loops: context explosion on long tasks, unrecoverable state, runaway token costs, and missing permission isolation. Work-rest discipline keeps each agent's context bounded (one task at a time, summaries persisted every day), events decide whether an agent should wake up (0-token filtering), and per-role computers give every agent an isolated filesystem.
Status: actively developed. The engine, role system, toolkits, Web UI and persistence are in place and covered by 205 JUnit tests; the simulation flow itself keeps being refined.
- Key Ideas
- Repository Layout
- Requirements
- Build & Test
- Running the Simulation
- How a "Day" Works
- Architecture Overview
- Core Features
- Configuration Reference
- Multiple AgentSystems in One Process
- Documentation
- Shift-driven on a simulated calendar clock, not loop-driven. A shared
TimeEventBusprovides the clock and the event bus at the same time. The simulation runs on a calendar clock with second-level ticks: by default 1 Tick = 1 simulated second (configurable), each work day starts at 08:00:00 of a real calendar date (day 1 = the day the run starts,SHIFT_START) and the shift ends at 18:00:00 (SHIFT_END, tick 36000 by default). The clock advances while roles are busy (by default in real time β 1 simulated second per real second of team work), fast-forwards to the next event Tick once the whole team has been idle for a while, and after the end-of-day summaries rolls over to the next day at 08:00:00 and loops β nobody waits in real time. - 0-token event filtering. Every event passes a per-role 3-layer filter (state mask β keyword salience β wake) before it ever costs a token. Irrelevant events are dropped for free, which is what keeps a large team affordable.
- One employee = one computer. Each role owns a Podman container (
maf-<role_id>on themaf-netbridge network), built from the project'sContainerfile. The container's home directory is the host folderdata/computers/<role_id>β the same files, visible both ways. Tools, notes, tasks and MCP servers live inside that computer, so permissions are naturally isolated. - A believable little company. Roles belong to groups (Leadership, Frontend/Backend/Mobile/
Full-Stack Development, Testing, Security, β¦), chat within their group (
talk), use company email (virtual mailbox, optionally real SMTP), hire new colleagues on the spot (post_job_posting), and the CEO talks to you β the client β through the console or the Web UI. - Java 21+ virtual threads. Each role runs on its own resident virtual thread; LLM requests
use the JDK
HttpClientwith retry semantics (429 / 5xx / timeouts). - Everything is configurable, JSON-first. Role templates live in
role_templates.json, the LLM-provider catalog inproviders.default.json, MCP tool groups inmcp_group_rules.json.
AgentSoftware/
βββ pom.xml # Maven build (artifact com.maf:agent-software)
βββ Containerfile # podman base image (Ubuntu 24.04) for role computers
βββ docs/
β βββ agent-system-multi-instance.md # multiple AgentSystems in one process (analysis + guide)
β βββ llm-provider-manager.md # LLM provider manager guide
βββ src/main/
β βββ java/com/agent/software/
β β βββ AgentSystem.java # self-contained team system (clock + pool + dispatch + stores)
β β βββ Main.java # main entry: full multi-day team simulation
β β βββ core/ # Types (Event / AgentState / Priority), MCPServer (stdio JSON-RPC)
β β βββ event/ # EventBus, TimeEventBus (time Γ event), EventDispatcher
β β βββ conversation/ # Conversation/ConversationManager β role β LLM API dialogue state
β β β # (per-day continuity, auto-compaction, shift-close, persistence)
β β βββ role/ # AgentRole, RolePool, RoleLoader (JSON templates), RoleFactory, ToolRegistry
β β βββ computers/ # Computer, ComputerManager, PodmanComputer, SSHComputer (kind: podman|local|ssh)
β β βββ io/ # Input / StdInput (console) / WebInput (Web page) β client replies
β β βββ llm/ # LLM, OpenAICompatLLM (layered config, retries)
β β β βββ provider/ # ProviderManager + provider catalog (18 providers, 2 wire dialects)
β β βββ tools/ # Tool / Toolkit base classes + registry
β β β βββ toolkits/ # memory, note, time, todo, taskview, pc, mcp, skill, email,
β β β # hermes, talk, hr, client (one Toolkit + one Tool per function)
β β βββ services/MailService.java# company email: virtual mailbox / real SMTP (jakarta.mail)
β β βββ store/ # ConfigStore, StateStore, NoteStore, TodoStore, PathManager (XDG)
β β βββ web/ # ChatWebServer + ChatStore (zero-dependency Web UI, jdk httpserver)
β β βββ utils/Json.java # Jackson helpers
β β βββ demo/ # RoleDemo / TalkDemo / McpDemo / WebDemo (lightweight demos)
β βββ resources/
β βββ role_templates.json # 55 role templates (role_id β role config)
β βββ providers.default.json # LLM provider catalog (OpenAI/Anthropic/Gemini/DeepSeek/β¦)
β βββ providers.local.example.json
β βββ mcp_group_rules.json # MCP servers + tool groups (file_ops, git_ops, github_ops)
β βββ web/ # static assets of the Web UI (index.html/app.js/style.css)
βββ src/test/java/ # JUnit 5 tests (205 tests / 28 classes)
βββ data/ # runtime data (gitignored)
βββ computers/<role_id>/ # one host folder per role computer (mounted at /home/agent)
βββ journals/ # per-role activity journals
βββ mail/ notes/ todos/ skills/ drive/ state.json
- JDK 25+ and Maven 3.8+ (
pom.xmlcompiles withmaven.compiler.release=25). - An OpenAI-compatible API key (export
OPENAI_API_KEY). Any compatible endpoint works β DeepSeek, vLLM, Ollama, LM Studio, β¦ β by settingOPENAI_BASE_URL/OPENAI_MODEL(keyless local endpoints can omit the key). - Podman for full simulations where every role gets its own container. Without podman,
set the computer kind to
local(see Personal computers); theMaindemo defaults to podman computers.
mvn compile # compile
mvn test # run all JUnit tests (205 tests, 28 test classes)
mvn package # produce target/agent-software.jar| Entry point | Class | What it does |
|---|---|---|
| Main simulation | com.agent.software.Main |
The full default team (47 roles) over multiple days: restore saved progress β shift start β tasks β shift end β save. Starts the Web UI automatically and asks you for the project requirements on Day 1 (console prompt). |
| Role demo | com.agent.software.demo.RoleDemo |
Creates one role from code (builder), assembles its tools and runs a real LLM task. |
| Talk demo | com.agent.software.demo.TalkDemo |
A small team chatting through the talk tool (collaboration chain: coder β reviewer β architect). |
| MCP demo | com.agent.software.demo.McpDemo |
Installs and calls MCP tools on a personal computer. |
| Web demo | com.agent.software.demo.WebDemo |
Lightweight Web-UI demo (4 roles, no LLM/computers): shows group chat + a Client A conversation where you reply in the browser. |
Run any entry point through the exec plugin, e.g.:
# full multi-role simulation (recommended first run)
export OPENAI_API_KEY="sk-..."
mvn exec:java -Dexec.mainClass=com.agent.software.Main
# lightweight demos
mvn exec:java -Dexec.mainClass=com.agent.software.demo.RoleDemo
mvn exec:java -Dexec.mainClass=com.agent.software.demo.TalkDemo
mvn exec:java -Dexec.mainClass=com.agent.software.demo.WebDemo # browser input; prints the URLWhat a Main run looks like:
- The system is assembled from the 47 default roles (JSON templates) and progress is
restored from
data/state.jsonif it exists; otherwise it starts fresh on Day 1. - The Web UI starts at
http://127.0.0.1:8787/(printed at startup). - Shortly after the 08:00:00 shift start (Tick 60 β 08:01:00), the CEO asks you for the
project requirements at the console prompt (with
WebDemo, you type the reply on the Web page instead). Enter something like "build a payment system for me". - Events unfold automatically: shift start, task assignment, tool calls, group chats, shift-end summaries. Interrupt with Ctrl+C β state is saved automatically and resumed on the next run.
The clock is a simulated calendar clock living in event/TimeEventBus.java. Day 1 is the
calendar date the run starts on (08:00:00); every simulated day after that is the next calendar
date. Key constants (default geometry = 1 Tick = 1 simulated second):
| Constant | Default | Meaning |
|---|---|---|
secondsPerTick (SECONDS_PER_TICK) |
1.0 | the Tick β simulated-time conversion (simulated seconds per tick); changing it rescales the geometry below |
SIM_SECONDS_PER_SHIFT |
36000 | the 10 h shift (08:00:00 β 18:00:00) in simulated seconds |
SIM_SECONDS_PER_DAY |
86400 | a full calendar day (08:00 β next 08:00) in simulated seconds |
shiftStartTick |
0 | tick 0 of each day β 08:00:00 (SHIFT_START fires) |
shiftEndTick |
36000 | shift ends at tick 36000 β 18:00:00 (SHIFT_END fires) |
ticksPerDay |
86400 | day cycle: 86400 ticks = 24 h at 1 s/tick (the after-hours window is skipped by the rollover jump) |
taskTickMin / taskTickMax |
0 / 36000 | note reminders can only be scheduled inside the shift (08:00:00β18:00:00) |
simSecondsPerRealSecond (SIM_SECONDS_PER_REAL_SECOND) |
1.0 | busy-clock speed: simulated seconds per real second of team work (1.0 = real-time flow) |
WRAP_UP_GRACE_SECONDS |
600 | real seconds the clock waits for the daily wrap-up before forcing the rollover |
FAST_FORWARD_IDLE_SECONDS |
60 | clock jumps when all roles have been idle β₯ 60 s |
Examples at the default conversion: tick 0 β 08:00:00, tick 21600 β 14:00:00, tick 36000 β
18:00:00, tick 86400 β the next day's 08:00:00.
How the clock moves (event-driven, not real-time):
- Busy flow. While at least one role is working (LLM round, tool call, talk wait), the clock advances at the busy-clock speed β with the default real-time pacing, a task that really takes 40 s advances the simulated clock by 40 seconds (a task started at 14:23:10 may genuinely finish at 14:23:50). Busy work can never push the clock past 18:00:00: anything still queued at shift end is held and carried over to the next day (off-duty roles do not start new ordinary work after 18:00).
- Idle fast-forward. When the whole team has been idle for
FAST_FORWARD_IDLE_SECONDS, the clock jumps to the next scheduled event Tick (a note reminder / the 18:00:00 shift end / β¦) β nobody waits in real time and a busy LLM never "misses" a deadline. - Wrap-up & day rollover. At 18:00:00
SHIFT_ENDfires: roles synchronously stuck in atalkwait are woken first (otherwise they would never reach their summary task), then every role writes its end-of-day summary and goesOFF_DUTY. Once all roles have wrapped up and the team is idle, the clock rolls over to the next calendar day at 08:00:00 (SHIFT_STARTfires again) and the loop repeats. If a role's summary failed and the wrap-up stalls, the time manager forces the rollover afterWRAP_UP_GRACE_SECONDSinstead of deadlocking.
Work-rest events fire automatically: SHIFT_START (08:00:00), SHIFT_END (18:00:00). Scheduled
notes with a reminder (write_note with remind_tick, ticks 0~36000 β 08:00:00β18:00:00) are
registered on the same event schedule and fire a reminder event when due β notes and scheduled
tasks are one unified concept. The simulated date/time (calendar date + HH:MM:SS) is shown by
get_time, in the shift-event payloads the roles read, on the Web UI header, and in journal/
console output.
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β AgentSystem (one per "company", self-contained) β
β owns: TimeEventBus Β· RolePool Β· EventDispatcher β
β ComputerManager Β· MailService Β· MCPManager β
β SkillManager Β· clientLock Β· ChatStore Β· dataDir β
βββββββββββββββββ¬βββββββββββββββββββββββββββββββββββββββββββββββ
β SHIFT_START / SHIFT_END / TASK_DUE / NEW_MAIL / custom events
βΌ
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β EventDispatcher.trigger(event) β fan-out to every role β
β per-role 3-layer filter: state mask β salience β wake β
βββββββββββββββββ¬βββββββββββββββββββββββββββββββββββββββββββββββ
β accepted events become Tasks
βΌ
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β RolePool β one virtual thread per role, priority queue β
β (CRITICAL > HIGH > NORMAL > LOW) β
βββββββββββββββββ¬βββββββββββββββββββββββββββββββββββββββββββββββ
β AgentRole.executeWithTools: LLM β tool_calls β execute β feedback
βΌ
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β AgentRole's personal computer (podman container) β
β Β· MCP servers run inside the container (podman exec) β
β Β· default MCP group file_ops auto-installed at role setup β
β Β· tools: notes/time/todo/taskview/pc/mcp/skill/email/talkβ¦ β
β Β· talk (same group) / email (cross group) / talk_to_client β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
All role definitions are JSON, not code: role_templates.json maps role_id β role config
(name, pinyin username, title, responsibilities, personality, skills, interest keywords,
group, β¦) and is loaded into RoleLoader.TEMPLATES at class-load time. 55 templates exist;
RoleLoader.DEFAULT_ROLES selects the 47-role default team used by Main:
- Leadership Group: CEO Lin Zong, COO Chen Zong, HR Wang Renshi, CTO Gao Yuan, business analyst Xu Ruonan
- Leads & release: frontend/backend/mobile/fullstack/test leads, architect Wang Jianguo, release manager Fang Jinyan
- Engineering: frontend_dev_1β3, backend_dev_1β3, mobile_dev_1β3, fullstack_dev_1β3
- Testing: tester_1β20 Β· Security: attacker_1β3
Other templates (CFO Qian Cai, reviewer, qa_engineer, ops_engineer, content_marketer,
data_analyst, support_agent, β¦) stay available through the RoleLoader / AgentSystem API.
Roles can also be built in code with the AgentRole.builder() fluent API.
- Every role is an
AgentRole: a task queue, a state machine (ON_DUTY/OFF_DUTY/WAIT/ wrapping-up), its own LLM client, its own stores (notes/todos) and its own activity journal. RolePoolschedules roles: one resident virtual thread per role, a priority task queue (CRITICAL > HIGH > NORMAL > LOW), dynamic onboarding (addRoleAndStartβ hire and start immediately) and removal.- The
talktoolkit is auto-registered when the pool starts; default toolkits are wired byRolePool.setupRole(see Β§4). - Tasks execute as an LLM tool loop (
executeWithTools): native function calling withtool_choice:"auto", results fed back withrole:"tool"+tool_call_id, and a bounded number of tool-call rounds per task. Trace messages (chain of thought, tool calls, final answer) are recorded and surfaced in the Web UI.
During dispatch each role evaluates every event independently β low-value events are intercepted at zero tokens:
| Layer | Mechanism | Token cost |
|---|---|---|
| 1 β State mask | OFF_DUTY roles ignore non-EMERGENCY events |
0 |
| 2 β Salience | keyword hits + urgency weighting (priority*0.4 + relevance*0.6) |
0 |
| 3 β Wake | events that pass become queued Tasks for that role | on demand |
System-time events (SHIFT_START / SHIFT_END, source="time") bypass Layer 2 and go straight
through. Inside an AgentSystem, mail delivery additionally dispatches a targeted
email/NEW_MAIL event to the recipient role, which queues a "You have a new email from X"
task β without disturbing off-duty roles.
One Toolkit subclass per domain, one Tool subclass per function
(tools/ + tools/toolkits/**), registered through ToolRegistry and exposed to the LLM as
OpenAI-style function schemas. Toolkits.defaultToolkits(role) auto-assembles for every role
(when autoToolkits=true):
| Toolkit | Tools | Purpose |
|---|---|---|
memory.Memory |
summary |
end-of-day summary (memory + triggers power-off) |
note.Note |
write_note edit_note list_notes read_note delete_note |
notes; write_note with a reminder = scheduled event |
time.Time |
get_time take_rest |
work-rest schedule |
todo.Todo |
todo_add todo_list todo_update todo_delete |
personal todo list |
taskview.TaskView |
my_tasks |
the role's task queue + history |
pc.Pc |
run_command computer_status lan_devices reboot |
operations on its own computer |
mcp.McpManager |
mcp_search mcp_list mcp_add mcp_remove mcp_my_tools |
self-service MCP tool management |
skill.Skill |
skill_search skill_list skill_add skill_remove skill_my_skills |
SKILL.md skill library |
email.Email |
send_email read_mail open_mail mail_address_book |
company email |
client.Client |
talk_to_client |
talking to you, the client (Leadership Group members) |
hr.Hr |
post_job_posting list_candidates |
hiring (HR; added explicitly in Main) |
talk.Talk |
talk list_roles |
inter-role chat, auto-registered at pool start |
Exclusive/conditional wiring: the CEO (and the rest of the Leadership Group) can reach the
client; HR can post jobs. hermes.Hermes (hermes_new_conversation / hermes_send) is also
available for calling a Hermes agent conversationally.
- Notes are the external memory of a role (they live in its own computer / note store) and
can carry a
remind_tickβ they become a scheduled event, like a task. - Daily summaries: at shift end each role calls
summary; the next daybuildSystemPrompt()injects the most recent summary as[Yesterday's Summary]so the role can continue its work. - Journals: every role has an activity journal under
data/journals/<role_id>.md(gitignored) recording tasks, tool calls, messages, WAIT transitions and accepted/skipped events β useful to review the whole team's activity in one place.
ComputerManagercreates aComputerper role: podman (default) |local|ssh. Without podman installed, podman computers throw at construction β uselocalfor plain directory-based simulation.- Podman computers run as containers named
maf-<role_id>on the custom bridge networkmaf-net. The base imagemaf-base:latestis defined by the rootContainerfile(Ubuntu 24.04 with Aliyun mirrors, Node 22 LTS, sudo/git/python, and the MCP servers preinstalled: the official filesystem server + Microsoft@playwright/mcpbrowser automation with chromium baked in) and is built automatically on first use. - The role's host folder
data/computers/<role_id>is mounted at the container's/home/agent(the same files visible from both sides, with a pinyinusernameand a stable uid per employee). A shared corporate cloud drive is mounted at/mnt/drive(Public+ per- employee directories). - Computers are powered off at shift end right after the daily summary is saved and started
again when work resumes. MCP servers run inside the container via
podman exec; each computer automatically starts two stdio sessions β the filesystem server (file tools) and the Playwright browser server@playwright/mcp(headless chromium,browser_*tools) β and their tools show up inmcp_search/mcp_listfor the role to install viamcp_add. Session liveness is probed at each shift start so servers are rebuilt automatically after a stop.
Each employee gets <username>@<suffix> (default suffix company.com; override per role with
an explicit email field). Default is a virtual mailbox persisted under data/mail/;
setting SMTP_HOST switches delivery to real SMTP (jakarta.mail) while still keeping an
internal copy for the simulated intranet. SMTP failures return an error instead of silently
dropping mail.
- Every role belongs to a group (see the template JSON).
talkis restricted to same-group members; cross-group communication goes through email. New hires are ungrouped and can talk to anyone. - Hire-to-onboard: HR calls
post_job_postingβRoleFactorygenerates a new role from a template (unique name from the name pool) β the hire is added withaddRoleAndStartand immediately starts working with the full default toolkit + MCP file tools + its own computer.
StateStore aggregates all serializable state into a single JSON file (default
data/state.json, atomic writes): role profiles, task history, incomplete (queued) tasks,
computer/container bindings (existing containers are re-bound, not rebuilt) and the clock
(day / tick of day / the simulated calendar base_date of day 1). Main auto-saves on exit and
auto-restores on startup, so a simulation can resume from where it stopped β with the same
calendar dates.
OpenAICompatLLMtalks to any OpenAI-compatible endpoint over the JDKHttpClient, with layered config resolution: explicit constructor args β Java args (-Dβ¦) β environment variables β config file (llm.*keys) β defaults (https://api.openai.com,gpt-4o-mini).llm.provider.ProviderManagerbundles a provider catalog (providers.default.json, 18 providers: OpenAI, Anthropic, Google Gemini, DeepSeek, Mistral, Groq, OpenRouter, Together, xAI, Moonshot, Zhipu, DashScope, SiliconFlow, Cerebras, NVIDIA, Ollama, vLLM, LM Studio) with base URLs, request paths and the wire dialect each speaks (OpenAI or Anthropic). It merges in an optional localproviders.json(seeproviders.local.example.json; setLLM_PROVIDERS_CONFIGor-Dllm.providers.configto point at it) and can list models through each provider's/modelsendpoint, normalizing both dialects intoModelInfo. See docs/llm-provider-manager.md.
A zero-dependency Web UI (ChatWebServer on the JDK com.sun.net.httpserver, static assets in
src/main/resources/web/) starts automatically with Main and can also be run standalone via
WebDemo. Open the printed URL (default http://127.0.0.1:8787/):
- Left β channels: the top All Activity feed shows every role's chain of thought, tool calls and final outputs; below it each group (Leadership Group, Frontend Development Group, β¦) shows its member count and an unread badge.
- Right β messages rendered by kind: π¬
talk/clientchat bubbles, π§reason(chain of thought from the LLM'sreasoning_content), βnotenarration, πtoolcall cards (name + arguments + output), β/βanswer(final task result). Leadership Group chats include your own (Client A) conversations. - Input box: enabled only when the Leadership Group is selected and a member is currently
waiting for your reply via
talk_to_client; type your answer and press Enter. - Pause / Resume (top-right button): pauses the whole simulation β the simulated clock
freezes (no shift events / task reminders fire), no role starts a new task, and in-flight LLM
requests stop being retried; click again (or
POST /api/resume) to continue with all held work. The system auto-pauses when the LLM API reports an exhausted account balance/quota (HTTP 402 or aninsufficient_quota/ "Insufficient Balance" /δ½ι’δΈθΆ³β¦ error) so an empty account is never hammered with retries β the top bar turns amber and shows the reason until you resume it. Pause/resume events appear in the chat feed as "System paused/resumed" notices.
The client's reply channel is decided by the Input passed to the AgentSystem at creation:
StdInput (console prompt) or WebInput (page input box, default 20-minute reply timeout,
returns an error instead of blocking when no browser is attached).
HTTP API (polled by the frontend, no auth):
| Endpoint | Description |
|---|---|
GET /api/state |
group roster + clock (day / tick / date / time / describe) + Client A conversation state (clientTalk.active) + pause state (paused / pauseReason) |
GET /api/messages?since=N |
incremental messages with seq > N |
POST /api/reply {"text": "β¦"} |
submit a Client A reply (409 unless someone is waiting) |
POST /api/pause {"reason": "β¦"} |
pause the whole simulation (reason optional; default "paused via the Web UI") |
POST /api/resume |
resume a paused simulation |
POST /api/attach |
Web attach heartbeat |
Message kinds: talk, client, reason, note, tool, answer; trace messages carry
structured extra metadata (tool: {tool, args, result, round, taskId}; answer:
{status: done|failed, tokens, taskId}). Implementation: web/ChatStore.java (storage +
Client A coordination) + web/ChatWebServer.java; tests in ChatStoreTest /
ChatWebServerTest / TalkToClientWebTest / AgentRoleTraceTest.
ToolRegistryis the unified registry: registering aTool/Toolkitimmediately makes it callable by the LLM (OpenAI-style schemas auto-generated from flat parameter descriptions).- MCP self-service:
McpManager(per system) + themcptoolkit let roles search, install and remove MCP tool groups (mcp_group_rules.jsondefinesfile_ops,git_ops,github_ops,default). The defaultfile_opsgroup (filesystem read/write/edit over the MCP stdio client implemented incore/MCPServer.java) is auto-installed at role setup. - Skills:
SkillManager+ theskilltoolkit manage SKILL.md entries in the shared skill library.
Each role now runs a managed dialogue with its LLM API (conversation/Conversation.java +
conversation/ConversationManager.java), sitting exactly between AgentRole and OpenAICompatLLM:
- Cross-task continuity. Previously every task started from an empty message list (system
prompt + the task description), so a role could not remember within a day what it had just
discussed or done with the model. Now
executeWithToolsprepares each request as system prompt + committed conversation history + new task, and when a task completes its exchange (user task + final answer, enriched with a bounded recap of the tool calls the role made) is committed back to the day dialogue β the next task builds on it. - Automatic context compaction. Each conversation has a character budget
(
Conversation.DEFAULT_MAX_HISTORY_CHARS, 24 000 by default; the budget is a per-conversation constructor argument). When the committed history crosses it, the whole history is summarized into one compact message viaLLM.summarize(one extra API call per budget crossing); if the summary call fails (or no LLM is at hand), the oldest messages are dropped/truncated instead β the context always shrinks, never grows without bound. - Shift lifecycle. Conversations are keyed by work day. When a role writes its end-of-day
summary and goes
OFF_DUTY, thesummarytool callsConversation.closeDay(): the day dialogue is cleared (its recap is already persisted in the day's summary file) and the trailing "summary saved" exchange is not appended. A new shift start β or a same-day EMERGENCY task after off-duty β reopens the conversation with a clean, context-flushed slate; the next day's cold start still comes from[Yesterday's Summary]in the system prompt. - Persistence & isolation. Open day dialogues are archived with the rest of the role state by
StateStore(conversationfield per role, restored on the next start), so an interrupted run resumes mid-dialogue. EachAgentSystemowns its ownConversationManager, so multiple systems in one process keep their role dialogues fully isolated.
One Conversation exists per role (keyed by role_id), accessed through AgentRole.conversation();
tool results are recapped at most TOOL_RECAP_LIMIT per task and TOOL_RECAP_RESULT_MAX chars
each. Tests: conversation/ConversationTest, conversation/ConversationEndToEndTest,
conversation/ConversationStateStoreTest.
All of these can be provided as environment variables or as Java args
(-D<same name>, which win over env vars). LLM values can additionally be placed in the
ConfigStore file (<config dir>/config.json, dot keys llm.api_key / llm.base_url /
llm.model); precedence: constructor args > -D system properties > env vars > config file >
defaults.
Variable (env / -D) |
Default | Description |
|---|---|---|
OPENAI_API_KEY |
(empty) | API key for the OpenAI-compatible endpoint (omitted β no Authorization header; fine for keyless local endpoints) |
OPENAI_BASE_URL |
https://api.openai.com |
base URL of any OpenAI-compatible API (DeepSeek / vLLM / Ollama / β¦) |
OPENAI_MODEL |
gpt-4o-mini |
model name |
LLM_PROVIDERS_CONFIG |
(see doc) | path of a local providers JSON for ProviderManager (overrides/extensions to the catalog) |
MAIL_SUFFIX |
company.com |
company email domain suffix (β guoxiaodong@company.com) |
MAIL_DATA_DIR |
data/mail |
where virtual mailboxes are persisted |
SMTP_HOST |
(empty) | SMTP server; setting it switches mail from virtual to real sending |
SMTP_PORT |
587 |
SMTP port (465 auto-uses SSL) |
SMTP_USER / SMTP_PASSWORD |
(empty) | SMTP credentials (optional) |
SMTP_FROM |
SMTP_USER or sender |
real-mail sender address (optional) |
SMTP_USE_SSL |
by port | force SSL (true) or non-SSL (false) |
AGENTSOFTWARE_WEB_HOST |
0.0.0.0 |
Web UI listen address |
AGENTSOFTWARE_WEB_PORT |
8787 |
Web UI port |
AGENTSOFTWARE_CLIENT_REPLY_TIMEOUT |
1200000 (20 min) |
Client A reply timeout in Web-input mode (ms) |
AGENTSOFTWARE_SECONDS_PER_TICK |
1.0 |
Tick β simulated-time conversion: simulated seconds that one tick represents (default 1 Tick = 1 simulated second; rescales the shift/day tick geometry) |
AGENTSOFTWARE_SIM_SECONDS_PER_REAL_SECOND |
1.0 |
busy-clock speed: simulated seconds that pass per real second while at least one role is working (0 freezes the clock while busy) |
AGENTSOFTWARE_DATA_DIR / _CONFIG_DIR / _CACHE_DIR / _LOG_DIR |
XDG dirs | path overrides for data / config / cache / log directories (PathManager, app prefix AgentSoftware) |
Note: the simulation runtime of an AgentSystem roots its own files under its dataDir
(default ./data), while PathManager XDG-style directories are used for config/log/cache
lookups such as the ConfigStore file.
Each AgentSystem directly owns its collaboration objects β clock, computer registry, mailbox,
MCP/skill managers, Client A communication lock, chat storage β plus its own data directory, so
several systems can safely coexist in one JVM (full analysis in
docs/agent-system-multi-instance.md):
import com.agent.software.AgentSystem;
import com.agent.software.io.StdInput;
import java.nio.file.Paths;
import java.util.List;
AgentSystem companyA = new AgentSystem(Paths.get("data/company-a"), null,
List.of("CEO", "COO", "HR"), 30.0, true, new StdInput());
AgentSystem companyB = new AgentSystem(Paths.get("data/company-b"), null,
List.of("CEO", "COO", "HR"), 30.0, true, new StdInput());Still process-wide on purpose: the role-template registry, default LLM config and host-level
podman infrastructure (networks / base images / container names). Two podman-based systems on the
same host must therefore use disjoint role sets β or local computers, each rooted in its own
base_dir.
- docs/agent-system-multi-instance.md β making
AgentSystemself-contained; running multiple systems in one process. - docs/llm-provider-manager.md β the LLM provider manager, catalog and local provider files.