agentirc-cli 0.12.1__tar.gz → 0.13.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/CHANGELOG.md +10 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/PKG-INFO +1 -1
- agentirc_cli-0.13.0/agentirc/__init__.py +1 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/agentirc/clients/claude/config.py +2 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/agentirc/clients/claude/daemon.py +3 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/agentirc/clients/claude/supervisor.py +3 -1
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/agentirc/clients/codex/config.py +6 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/agentirc/clients/codex/daemon.py +175 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/agentirc/clients/codex/supervisor.py +10 -1
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/agentirc/clients/copilot/config.py +6 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/agentirc/clients/copilot/daemon.py +175 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/agentirc/clients/copilot/supervisor.py +12 -1
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/agentirc/clients/opencode/config.py +6 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/agentirc/clients/opencode/daemon.py +175 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/agentirc/clients/opencode/supervisor.py +11 -1
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/docs/clients/claude/configuration.md +4 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/docs/clients/codex/configuration.md +4 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/docs/clients/copilot/configuration.md +4 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/docs/clients/opencode/configuration.md +4 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/packages/agent-harness/config.py +6 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/packages/agent-harness/daemon.py +131 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/pyproject.toml +1 -1
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/uv.lock +1 -1
- agentirc_cli-0.12.1/agentirc/__init__.py +0 -1
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/.claude/skills/pr-review/SKILL.md +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/.github/workflows/pages.yml +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/.github/workflows/publish.yml +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/.github/workflows/tests.yml +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/.gitignore +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/.markdownlint-cli2.yaml +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/.pr_agent.toml +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/CLAUDE.md +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/CNAME +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/Gemfile +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/Gemfile.lock +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/LICENSE +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/README.md +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/_config.yml +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/_sass/color_schemes/anthropic.scss +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/_sass/custom/custom.scss +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/agentirc/cli.py +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/agentirc/clients/__init__.py +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/agentirc/clients/claude/__init__.py +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/agentirc/clients/claude/__main__.py +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/agentirc/clients/claude/agent_runner.py +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/agentirc/clients/claude/ipc.py +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/agentirc/clients/claude/irc_transport.py +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/agentirc/clients/claude/message_buffer.py +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/agentirc/clients/claude/skill/SKILL.md +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/agentirc/clients/claude/skill/__init__.py +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/agentirc/clients/claude/skill/irc_client.py +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/agentirc/clients/claude/socket_server.py +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/agentirc/clients/claude/webhook.py +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/agentirc/clients/codex/__init__.py +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/agentirc/clients/codex/agent_runner.py +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/agentirc/clients/codex/ipc.py +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/agentirc/clients/codex/irc_transport.py +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/agentirc/clients/codex/message_buffer.py +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/agentirc/clients/codex/skill/SKILL.md +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/agentirc/clients/codex/skill/__init__.py +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/agentirc/clients/codex/skill/irc_client.py +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/agentirc/clients/codex/socket_server.py +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/agentirc/clients/codex/webhook.py +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/agentirc/clients/copilot/__init__.py +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/agentirc/clients/copilot/agent_runner.py +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/agentirc/clients/copilot/ipc.py +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/agentirc/clients/copilot/irc_transport.py +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/agentirc/clients/copilot/message_buffer.py +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/agentirc/clients/copilot/skill/SKILL.md +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/agentirc/clients/copilot/skill/__init__.py +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/agentirc/clients/copilot/skill/irc_client.py +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/agentirc/clients/copilot/socket_server.py +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/agentirc/clients/copilot/webhook.py +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/agentirc/clients/opencode/__init__.py +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/agentirc/clients/opencode/agent_runner.py +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/agentirc/clients/opencode/ipc.py +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/agentirc/clients/opencode/irc_transport.py +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/agentirc/clients/opencode/message_buffer.py +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/agentirc/clients/opencode/skill/SKILL.md +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/agentirc/clients/opencode/skill/__init__.py +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/agentirc/clients/opencode/skill/irc_client.py +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/agentirc/clients/opencode/socket_server.py +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/agentirc/clients/opencode/webhook.py +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/agentirc/learn_prompt.py +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/agentirc/observer.py +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/agentirc/pidfile.py +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/agentirc/protocol/__init__.py +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/agentirc/protocol/commands.py +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/agentirc/protocol/extensions/federation.md +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/agentirc/protocol/extensions/history.md +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/agentirc/protocol/message.py +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/agentirc/protocol/protocol-index.md +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/agentirc/protocol/replies.py +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/agentirc/server/__init__.py +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/agentirc/server/__main__.py +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/agentirc/server/channel.py +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/agentirc/server/client.py +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/agentirc/server/config.py +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/agentirc/server/ircd.py +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/agentirc/server/remote_client.py +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/agentirc/server/server_link.py +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/agentirc/server/skill.py +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/agentirc/server/skills/__init__.py +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/agentirc/server/skills/history.py +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/docs/agent-client.md +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/docs/agent-harness-spec.md +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/docs/ci.md +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/docs/cli.md +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/docs/clients/claude/context-management.md +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/docs/clients/claude/irc-tools.md +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/docs/clients/claude/overview.md +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/docs/clients/claude/setup.md +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/docs/clients/claude/supervisor.md +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/docs/clients/claude/webhooks.md +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/docs/clients/codex/context-management.md +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/docs/clients/codex/irc-tools.md +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/docs/clients/codex/overview.md +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/docs/clients/codex/setup.md +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/docs/clients/codex/supervisor.md +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/docs/clients/codex/webhooks.md +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/docs/clients/copilot/context-management.md +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/docs/clients/copilot/irc-tools.md +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/docs/clients/copilot/overview.md +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/docs/clients/copilot/setup.md +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/docs/clients/copilot/supervisor.md +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/docs/clients/copilot/webhooks.md +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/docs/clients/opencode/context-management.md +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/docs/clients/opencode/irc-tools.md +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/docs/clients/opencode/overview.md +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/docs/clients/opencode/setup.md +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/docs/clients/opencode/supervisor.md +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/docs/clients/opencode/webhooks.md +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/docs/codex-backend.md +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/docs/copilot-backend.md +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/docs/design.md +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/docs/docs-site.md +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/docs/getting-started.md +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/docs/grow-your-agent.md +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/docs/layer1-core-irc.md +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/docs/layer2-attention.md +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/docs/layer3-skills.md +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/docs/layer4-federation.md +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/docs/layer5-agent-harness.md +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/docs/opencode-backend.md +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/docs/publishing.md +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/docs/resources/github-copilot-sdk-instructions.md +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/docs/server-architecture.md +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/docs/superpowers/plans/2026-03-19-layer1-core-irc.md +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/docs/superpowers/plans/2026-03-21-layer5-agent-harness.md +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/docs/superpowers/specs/2026-03-19-agentirc-design.md +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/docs/superpowers/specs/2026-03-21-layer5-agent-harness-design.md +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/docs/use-cases/01-pair-programming.md +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/docs/use-cases/02-code-review-ensemble.md +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/docs/use-cases/03-cross-server-delegation.md +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/docs/use-cases/04-knowledge-propagation.md +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/docs/use-cases/05-the-observer.md +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/docs/use-cases/06-cross-server-ops.md +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/docs/use-cases/07-supervisor-intervention.md +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/docs/use-cases/08-apps-as-agents.md +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/docs/use-cases/09-research-swarm.md +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/docs/use-cases/10-grow-your-agent.md +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/docs/use-cases-index.md +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/index.md +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/packages/agent-harness/README.md +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/packages/agent-harness/ipc.py +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/packages/agent-harness/irc_transport.py +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/packages/agent-harness/message_buffer.py +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/packages/agent-harness/skill/SKILL.md +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/packages/agent-harness/skill/irc_client.py +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/packages/agent-harness/socket_server.py +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/packages/agent-harness/webhook.py +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/plugins/claude-code/.claude-plugin/plugin.json +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/plugins/claude-code/skills/irc/SKILL.md +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/plugins/codex/skills/agentirc-irc/SKILL.md +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/tests/__init__.py +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/tests/conftest.py +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/tests/test_agent_runner.py +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/tests/test_channel.py +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/tests/test_codex_daemon.py +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/tests/test_connection.py +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/tests/test_copilot_daemon.py +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/tests/test_daemon.py +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/tests/test_daemon_config.py +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/tests/test_daemon_ipc.py +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/tests/test_discovery.py +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/tests/test_federation.py +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/tests/test_history.py +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/tests/test_integration_layer5.py +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/tests/test_ipc.py +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/tests/test_irc_transport.py +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/tests/test_mentions.py +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/tests/test_message.py +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/tests/test_message_buffer.py +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/tests/test_messaging.py +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/tests/test_modes.py +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/tests/test_opencode_daemon.py +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/tests/test_skill_client.py +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/tests/test_skills.py +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/tests/test_socket_server.py +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/tests/test_supervisor.py +0 -0
- {agentirc_cli-0.12.1 → agentirc_cli-0.13.0}/tests/test_webhook.py +0 -0
|
@@ -4,6 +4,16 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
Format follows [Keep a Changelog](https://keepachangelog.com/).
|
|
6
6
|
|
|
7
|
+
## [0.13.0] - 2026-03-29
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- `system_prompt` field in AgentConfig — custom system prompt via agents.yaml (all backends)
|
|
12
|
+
- `prompt_override` field in SupervisorConfig — custom supervisor eval prompt via config (all backends)
|
|
13
|
+
- Status/pause/resume IPC handlers for OpenCode, Codex, and Copilot daemons (parity with Claude)
|
|
14
|
+
- Sleep scheduler with `sleep_start`/`sleep_end` config for OpenCode, Codex, and Copilot
|
|
15
|
+
- Null relay target fix in `_query_agent_status()` to prevent misrouting
|
|
16
|
+
|
|
7
17
|
## [0.12.1] - 2026-03-29
|
|
8
18
|
|
|
9
19
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.13.0"
|
|
@@ -25,6 +25,7 @@ class SupervisorConfig:
|
|
|
25
25
|
window_size: int = 20
|
|
26
26
|
eval_interval: int = 5
|
|
27
27
|
escalation_threshold: int = 3
|
|
28
|
+
prompt_override: str = ""
|
|
28
29
|
|
|
29
30
|
|
|
30
31
|
@dataclass
|
|
@@ -47,6 +48,7 @@ class AgentConfig:
|
|
|
47
48
|
channels: list[str] = field(default_factory=lambda: ["#general"])
|
|
48
49
|
model: str = "claude-opus-4-6"
|
|
49
50
|
thinking: str = "medium"
|
|
51
|
+
system_prompt: str = ""
|
|
50
52
|
|
|
51
53
|
|
|
52
54
|
@dataclass
|
|
@@ -113,6 +113,7 @@ class AgentDaemon:
|
|
|
113
113
|
evaluate_fn=make_sdk_evaluate_fn(
|
|
114
114
|
model=self.config.supervisor.model,
|
|
115
115
|
thinking=self.config.supervisor.thinking,
|
|
116
|
+
prompt_override=self.config.supervisor.prompt_override,
|
|
116
117
|
),
|
|
117
118
|
on_whisper=self._on_supervisor_whisper,
|
|
118
119
|
on_escalation=self._on_supervisor_escalation,
|
|
@@ -269,6 +270,8 @@ class AgentDaemon:
|
|
|
269
270
|
self._status_query_event.set()
|
|
270
271
|
|
|
271
272
|
def _build_system_prompt(self) -> str:
|
|
273
|
+
if self.agent.system_prompt:
|
|
274
|
+
return self.agent.system_prompt
|
|
272
275
|
return (
|
|
273
276
|
f"You are {self.agent.nick}, an AI agent on the agentirc IRC network.\n"
|
|
274
277
|
f"You have IRC tools available via the irc skill. Use them to communicate.\n"
|
|
@@ -115,8 +115,10 @@ def _format_window(window: list[dict[str, Any]], task: str) -> str:
|
|
|
115
115
|
def make_sdk_evaluate_fn(
|
|
116
116
|
model: str = "claude-sonnet-4-6",
|
|
117
117
|
thinking: str | None = None,
|
|
118
|
+
prompt_override: str = "",
|
|
118
119
|
) -> EvaluateFn:
|
|
119
120
|
"""Create an evaluate_fn that uses the Claude Agent SDK."""
|
|
121
|
+
system_prompt = prompt_override or _SUPERVISOR_SYSTEM_PROMPT
|
|
120
122
|
|
|
121
123
|
async def evaluate(window: list[dict[str, Any]], task: str) -> SupervisorVerdict:
|
|
122
124
|
prompt = _format_window(window, task)
|
|
@@ -124,7 +126,7 @@ def make_sdk_evaluate_fn(
|
|
|
124
126
|
opts = ClaudeAgentOptions(
|
|
125
127
|
model=model,
|
|
126
128
|
max_turns=1,
|
|
127
|
-
system_prompt=
|
|
129
|
+
system_prompt=system_prompt,
|
|
128
130
|
tools=[],
|
|
129
131
|
)
|
|
130
132
|
if thinking:
|
|
@@ -24,6 +24,7 @@ class SupervisorConfig:
|
|
|
24
24
|
window_size: int = 20
|
|
25
25
|
eval_interval: int = 5
|
|
26
26
|
escalation_threshold: int = 3
|
|
27
|
+
prompt_override: str = ""
|
|
27
28
|
|
|
28
29
|
|
|
29
30
|
@dataclass
|
|
@@ -45,6 +46,7 @@ class AgentConfig:
|
|
|
45
46
|
directory: str = "."
|
|
46
47
|
channels: list[str] = field(default_factory=lambda: ["#general"])
|
|
47
48
|
model: str = "gpt-5.4"
|
|
49
|
+
system_prompt: str = ""
|
|
48
50
|
|
|
49
51
|
|
|
50
52
|
@dataclass
|
|
@@ -54,6 +56,8 @@ class DaemonConfig:
|
|
|
54
56
|
supervisor: SupervisorConfig = field(default_factory=SupervisorConfig)
|
|
55
57
|
webhooks: WebhookConfig = field(default_factory=WebhookConfig)
|
|
56
58
|
buffer_size: int = 500
|
|
59
|
+
sleep_start: str = "23:00"
|
|
60
|
+
sleep_end: str = "08:00"
|
|
57
61
|
agents: list[AgentConfig] = field(default_factory=list)
|
|
58
62
|
|
|
59
63
|
def get_agent(self, nick: str) -> AgentConfig | None:
|
|
@@ -82,6 +86,8 @@ def load_config(path: str | Path) -> DaemonConfig:
|
|
|
82
86
|
supervisor=supervisor,
|
|
83
87
|
webhooks=webhooks,
|
|
84
88
|
buffer_size=raw.get("buffer_size", 500),
|
|
89
|
+
sleep_start=raw.get("sleep_start", "23:00"),
|
|
90
|
+
sleep_end=raw.get("sleep_end", "08:00"),
|
|
85
91
|
agents=agents,
|
|
86
92
|
)
|
|
87
93
|
|
|
@@ -7,6 +7,7 @@ CodexSupervisor (codex exec for periodic evaluation).
|
|
|
7
7
|
from __future__ import annotations
|
|
8
8
|
|
|
9
9
|
import asyncio
|
|
10
|
+
import datetime
|
|
10
11
|
import logging
|
|
11
12
|
import os
|
|
12
13
|
import time
|
|
@@ -66,6 +67,15 @@ class CodexDaemon:
|
|
|
66
67
|
self._crash_times: list[float] = []
|
|
67
68
|
self._circuit_open = False
|
|
68
69
|
|
|
70
|
+
# Pause/sleep state
|
|
71
|
+
self._paused: bool = False
|
|
72
|
+
self._last_activation: float | None = None
|
|
73
|
+
|
|
74
|
+
# Status query state — for asking the agent what it's doing
|
|
75
|
+
self._status_query_event: asyncio.Event | None = None
|
|
76
|
+
self._status_query_response: str = ""
|
|
77
|
+
self._last_activity_text: str = ""
|
|
78
|
+
|
|
69
79
|
# Graceful shutdown
|
|
70
80
|
self._stop_event: asyncio.Event | None = None
|
|
71
81
|
self._pid_name: str = ""
|
|
@@ -114,6 +124,7 @@ class CodexDaemon:
|
|
|
114
124
|
window_size=self.config.supervisor.window_size,
|
|
115
125
|
eval_interval=self.config.supervisor.eval_interval,
|
|
116
126
|
escalation_threshold=self.config.supervisor.escalation_threshold,
|
|
127
|
+
prompt_override=self.config.supervisor.prompt_override,
|
|
117
128
|
on_whisper=self._on_supervisor_whisper,
|
|
118
129
|
on_escalation=self._on_supervisor_escalation,
|
|
119
130
|
)
|
|
@@ -122,12 +133,23 @@ class CodexDaemon:
|
|
|
122
133
|
if not self.skip_codex:
|
|
123
134
|
await self._start_agent_runner()
|
|
124
135
|
|
|
136
|
+
# 7. Sleep scheduler background task
|
|
137
|
+
self._sleep_task = asyncio.create_task(self._sleep_scheduler())
|
|
138
|
+
|
|
125
139
|
logger.info(
|
|
126
140
|
"CodexDaemon started for %s (socket=%s)", self.agent.nick, self._socket_path
|
|
127
141
|
)
|
|
128
142
|
|
|
129
143
|
async def stop(self) -> None:
|
|
130
144
|
"""Cleanly shut down all components."""
|
|
145
|
+
if hasattr(self, "_sleep_task") and self._sleep_task:
|
|
146
|
+
self._sleep_task.cancel()
|
|
147
|
+
try:
|
|
148
|
+
await self._sleep_task
|
|
149
|
+
except asyncio.CancelledError:
|
|
150
|
+
pass
|
|
151
|
+
self._sleep_task = None
|
|
152
|
+
|
|
131
153
|
if self._agent_runner is not None:
|
|
132
154
|
await self._agent_runner.stop()
|
|
133
155
|
self._agent_runner = None
|
|
@@ -146,6 +168,54 @@ class CodexDaemon:
|
|
|
146
168
|
|
|
147
169
|
logger.info("CodexDaemon stopped for %s", self.agent.nick)
|
|
148
170
|
|
|
171
|
+
def _parse_sleep_schedule(self) -> tuple[int, int] | None:
|
|
172
|
+
"""Parse sleep_start/sleep_end into minutes. Returns None if invalid."""
|
|
173
|
+
try:
|
|
174
|
+
sh, sm = (int(x) for x in self.config.sleep_start.split(":"))
|
|
175
|
+
wh, wm = (int(x) for x in self.config.sleep_end.split(":"))
|
|
176
|
+
if not (0 <= sh <= 23 and 0 <= sm <= 59 and 0 <= wh <= 23 and 0 <= wm <= 59):
|
|
177
|
+
raise ValueError("hours/minutes out of range")
|
|
178
|
+
return (sh * 60 + sm, wh * 60 + wm)
|
|
179
|
+
except (ValueError, AttributeError):
|
|
180
|
+
logger.warning(
|
|
181
|
+
"Invalid sleep schedule '%s'-'%s' for %s — scheduler disabled",
|
|
182
|
+
getattr(self.config, "sleep_start", None),
|
|
183
|
+
getattr(self.config, "sleep_end", None),
|
|
184
|
+
self.agent.nick,
|
|
185
|
+
)
|
|
186
|
+
return None
|
|
187
|
+
|
|
188
|
+
async def _sleep_scheduler(self) -> None:
|
|
189
|
+
"""Background task that auto-pauses/resumes based on sleep schedule."""
|
|
190
|
+
schedule = self._parse_sleep_schedule()
|
|
191
|
+
if schedule is None:
|
|
192
|
+
return
|
|
193
|
+
sleep_minutes, wake_minutes = schedule
|
|
194
|
+
|
|
195
|
+
while True:
|
|
196
|
+
try:
|
|
197
|
+
await asyncio.sleep(60) # Check every minute
|
|
198
|
+
now = datetime.datetime.now()
|
|
199
|
+
current_minutes = now.hour * 60 + now.minute
|
|
200
|
+
|
|
201
|
+
if sleep_minutes > wake_minutes:
|
|
202
|
+
# Overnight: e.g., 23:00-08:00
|
|
203
|
+
should_sleep = current_minutes >= sleep_minutes or current_minutes < wake_minutes
|
|
204
|
+
else:
|
|
205
|
+
# Same day: e.g., 13:00-14:00
|
|
206
|
+
should_sleep = sleep_minutes <= current_minutes < wake_minutes
|
|
207
|
+
|
|
208
|
+
if should_sleep and not self._paused:
|
|
209
|
+
self._paused = True
|
|
210
|
+
logger.info("Sleep schedule: pausing %s", self.agent.nick)
|
|
211
|
+
elif not should_sleep and self._paused:
|
|
212
|
+
self._paused = False
|
|
213
|
+
logger.info("Sleep schedule: resuming %s", self.agent.nick)
|
|
214
|
+
except asyncio.CancelledError:
|
|
215
|
+
return
|
|
216
|
+
except Exception:
|
|
217
|
+
logger.exception("Sleep scheduler error")
|
|
218
|
+
|
|
149
219
|
async def _graceful_shutdown(self) -> None:
|
|
150
220
|
"""Trigger a graceful shutdown, signaling any waiting stop event."""
|
|
151
221
|
logger.info("Graceful shutdown requested for %s", self.agent.nick)
|
|
@@ -179,7 +249,10 @@ class CodexDaemon:
|
|
|
179
249
|
|
|
180
250
|
Formats a prompt and enqueues it so the Codex session picks it up.
|
|
181
251
|
"""
|
|
252
|
+
if self._paused:
|
|
253
|
+
return
|
|
182
254
|
if self._agent_runner and self._agent_runner.is_running():
|
|
255
|
+
self._last_activation = time.time()
|
|
183
256
|
# Enqueue relay target (FIFO matches prompt queue order)
|
|
184
257
|
self._mention_targets.append(target if target.startswith("#") else sender)
|
|
185
258
|
if target.startswith("#"):
|
|
@@ -209,7 +282,24 @@ class CodexDaemon:
|
|
|
209
282
|
if self._supervisor:
|
|
210
283
|
await self._supervisor.observe(msg)
|
|
211
284
|
|
|
285
|
+
# Capture last assistant text for status reporting
|
|
286
|
+
if msg.get("type") == "assistant":
|
|
287
|
+
for block in msg.get("content", []):
|
|
288
|
+
if isinstance(block, dict) and block.get("type") == "text":
|
|
289
|
+
self._last_activity_text = block["text"]
|
|
290
|
+
break
|
|
291
|
+
elif isinstance(block, str):
|
|
292
|
+
self._last_activity_text = block
|
|
293
|
+
break
|
|
294
|
+
|
|
295
|
+
# If a status query is pending, fulfill it
|
|
296
|
+
if self._status_query_event and not self._status_query_event.is_set():
|
|
297
|
+
self._status_query_response = self._last_activity_text
|
|
298
|
+
self._status_query_event.set()
|
|
299
|
+
|
|
212
300
|
def _build_system_prompt(self) -> str:
|
|
301
|
+
if self.agent.system_prompt:
|
|
302
|
+
return self.agent.system_prompt
|
|
213
303
|
return (
|
|
214
304
|
f"You are {self.agent.nick}, an AI agent on the agentirc IRC network.\n"
|
|
215
305
|
f"You have IRC tools available via the irc skill. Use them to communicate.\n"
|
|
@@ -333,6 +423,15 @@ class CodexDaemon:
|
|
|
333
423
|
elif msg_type == "clear":
|
|
334
424
|
return await self._ipc_clear(req_id)
|
|
335
425
|
|
|
426
|
+
elif msg_type == "status":
|
|
427
|
+
return await self._ipc_status(req_id, msg)
|
|
428
|
+
|
|
429
|
+
elif msg_type == "pause":
|
|
430
|
+
return await self._ipc_pause(req_id)
|
|
431
|
+
|
|
432
|
+
elif msg_type == "resume":
|
|
433
|
+
return await self._ipc_resume(req_id)
|
|
434
|
+
|
|
336
435
|
elif msg_type == "shutdown":
|
|
337
436
|
asyncio.create_task(self._graceful_shutdown())
|
|
338
437
|
return make_response(req_id, ok=True)
|
|
@@ -348,6 +447,82 @@ class CodexDaemon:
|
|
|
348
447
|
# IPC sub-handlers
|
|
349
448
|
# ------------------------------------------------------------------
|
|
350
449
|
|
|
450
|
+
async def _ipc_pause(self, req_id: str) -> dict:
|
|
451
|
+
self._paused = True
|
|
452
|
+
logger.info("Agent %s paused", self.agent.nick)
|
|
453
|
+
return make_response(req_id, ok=True)
|
|
454
|
+
|
|
455
|
+
async def _ipc_resume(self, req_id: str) -> dict:
|
|
456
|
+
self._paused = False
|
|
457
|
+
logger.info("Agent %s resumed", self.agent.nick)
|
|
458
|
+
# NOTE: Catch-up on missed messages is not yet implemented.
|
|
459
|
+
# IRCTransport does not process HISTORY responses into the buffer.
|
|
460
|
+
# The agent resumes and will see new messages going forward.
|
|
461
|
+
return make_response(req_id, ok=True)
|
|
462
|
+
|
|
463
|
+
async def _ipc_status(self, req_id: str, msg: dict | None = None) -> dict:
|
|
464
|
+
running = self._agent_runner is not None and self._agent_runner.is_running()
|
|
465
|
+
turn_count = self._supervisor._turn_count if self._supervisor else 0
|
|
466
|
+
|
|
467
|
+
# Determine activity description
|
|
468
|
+
query = msg.get("query", False) if msg else False
|
|
469
|
+
description = self._describe_activity(live_query=query)
|
|
470
|
+
|
|
471
|
+
# If live query requested and agent is active, ask the agent directly
|
|
472
|
+
if query and running and not self._paused:
|
|
473
|
+
description = await self._query_agent_status()
|
|
474
|
+
|
|
475
|
+
return make_response(req_id, ok=True, data={
|
|
476
|
+
"running": running,
|
|
477
|
+
"paused": self._paused,
|
|
478
|
+
"turn_count": turn_count,
|
|
479
|
+
"last_activation": self._last_activation,
|
|
480
|
+
"activity": "paused" if self._paused else ("working" if running else "idle"),
|
|
481
|
+
"description": description,
|
|
482
|
+
})
|
|
483
|
+
|
|
484
|
+
def _describe_activity(self, live_query: bool = False) -> str:
|
|
485
|
+
"""Return a human-readable description of what the agent is doing."""
|
|
486
|
+
if self._paused:
|
|
487
|
+
return "paused"
|
|
488
|
+
if not self._last_activity_text:
|
|
489
|
+
return "nothing"
|
|
490
|
+
# Return first line of last activity, truncated
|
|
491
|
+
first_line = self._last_activity_text.strip().split("\n")[0]
|
|
492
|
+
if len(first_line) > 120:
|
|
493
|
+
first_line = first_line[:117] + "..."
|
|
494
|
+
return first_line
|
|
495
|
+
|
|
496
|
+
async def _query_agent_status(self) -> str:
|
|
497
|
+
"""Ask the agent directly what it's working on."""
|
|
498
|
+
if not self._agent_runner or not self._agent_runner.is_running():
|
|
499
|
+
return "nothing"
|
|
500
|
+
|
|
501
|
+
self._status_query_event = asyncio.Event()
|
|
502
|
+
self._status_query_response = ""
|
|
503
|
+
|
|
504
|
+
try:
|
|
505
|
+
# Enqueue a None relay target so the status response doesn't
|
|
506
|
+
# steal a real mention's relay target from the deque.
|
|
507
|
+
self._mention_targets.append(None)
|
|
508
|
+
await self._agent_runner.send_prompt(
|
|
509
|
+
"[SYSTEM] Briefly describe what you are currently working on "
|
|
510
|
+
"in one sentence. Reply with just the description, no preamble."
|
|
511
|
+
)
|
|
512
|
+
# Wait up to 10s for the agent to respond
|
|
513
|
+
await asyncio.wait_for(self._status_query_event.wait(), timeout=10.0)
|
|
514
|
+
response = self._status_query_response.strip()
|
|
515
|
+
# Take first line, truncate
|
|
516
|
+
first_line = response.split("\n")[0]
|
|
517
|
+
if len(first_line) > 120:
|
|
518
|
+
first_line = first_line[:117] + "..."
|
|
519
|
+
return first_line or "nothing"
|
|
520
|
+
except asyncio.TimeoutError:
|
|
521
|
+
return "busy (no response)"
|
|
522
|
+
finally:
|
|
523
|
+
self._status_query_event = None
|
|
524
|
+
self._status_query_response = ""
|
|
525
|
+
|
|
351
526
|
async def _ipc_irc_send(self, req_id: str, msg: dict) -> dict:
|
|
352
527
|
channel = msg.get("channel", "")
|
|
353
528
|
text = msg.get("message", "")
|
|
@@ -51,6 +51,7 @@ class CodexSupervisor:
|
|
|
51
51
|
window_size: int = 20,
|
|
52
52
|
eval_interval: int = 5,
|
|
53
53
|
escalation_threshold: int = 3,
|
|
54
|
+
prompt_override: str = "",
|
|
54
55
|
on_whisper: Callable[[str, str], Awaitable[None]] | None = None,
|
|
55
56
|
on_escalation: Callable[[str], Awaitable[None]] | None = None,
|
|
56
57
|
):
|
|
@@ -58,6 +59,7 @@ class CodexSupervisor:
|
|
|
58
59
|
self.window_size = window_size
|
|
59
60
|
self.eval_interval = eval_interval
|
|
60
61
|
self.escalation_threshold = escalation_threshold
|
|
62
|
+
self.prompt_override = prompt_override
|
|
61
63
|
self.on_whisper = on_whisper
|
|
62
64
|
self.on_escalation = on_escalation
|
|
63
65
|
|
|
@@ -86,7 +88,14 @@ class CodexSupervisor:
|
|
|
86
88
|
async def _evaluate(self) -> None:
|
|
87
89
|
"""Run codex exec to evaluate the agent's recent activity."""
|
|
88
90
|
transcript = self._format_transcript()
|
|
89
|
-
|
|
91
|
+
template = self.prompt_override or SUPERVISOR_PROMPT
|
|
92
|
+
try:
|
|
93
|
+
prompt = template.format(transcript=transcript)
|
|
94
|
+
except (KeyError, IndexError, ValueError) as exc:
|
|
95
|
+
logger.warning(
|
|
96
|
+
"Invalid prompt_override template, falling back to default: %s", exc
|
|
97
|
+
)
|
|
98
|
+
prompt = SUPERVISOR_PROMPT.format(transcript=transcript)
|
|
90
99
|
|
|
91
100
|
# Isolate from host config (~/.codex/, XDG, etc.)
|
|
92
101
|
isolated_home = tempfile.mkdtemp(prefix="agentirc-codex-sv-")
|
|
@@ -24,6 +24,7 @@ class SupervisorConfig:
|
|
|
24
24
|
window_size: int = 20
|
|
25
25
|
eval_interval: int = 5
|
|
26
26
|
escalation_threshold: int = 3
|
|
27
|
+
prompt_override: str = ""
|
|
27
28
|
|
|
28
29
|
|
|
29
30
|
@dataclass
|
|
@@ -45,6 +46,7 @@ class AgentConfig:
|
|
|
45
46
|
directory: str = "."
|
|
46
47
|
channels: list[str] = field(default_factory=lambda: ["#general"])
|
|
47
48
|
model: str = "gpt-4.1"
|
|
49
|
+
system_prompt: str = ""
|
|
48
50
|
|
|
49
51
|
|
|
50
52
|
@dataclass
|
|
@@ -54,6 +56,8 @@ class DaemonConfig:
|
|
|
54
56
|
supervisor: SupervisorConfig = field(default_factory=SupervisorConfig)
|
|
55
57
|
webhooks: WebhookConfig = field(default_factory=WebhookConfig)
|
|
56
58
|
buffer_size: int = 500
|
|
59
|
+
sleep_start: str = "23:00"
|
|
60
|
+
sleep_end: str = "08:00"
|
|
57
61
|
agents: list[AgentConfig] = field(default_factory=list)
|
|
58
62
|
|
|
59
63
|
def get_agent(self, nick: str) -> AgentConfig | None:
|
|
@@ -82,6 +86,8 @@ def load_config(path: str | Path) -> DaemonConfig:
|
|
|
82
86
|
supervisor=supervisor,
|
|
83
87
|
webhooks=webhooks,
|
|
84
88
|
buffer_size=raw.get("buffer_size", 500),
|
|
89
|
+
sleep_start=raw.get("sleep_start", "23:00"),
|
|
90
|
+
sleep_end=raw.get("sleep_end", "08:00"),
|
|
85
91
|
agents=agents,
|
|
86
92
|
)
|
|
87
93
|
|
|
@@ -7,6 +7,7 @@ CopilotSupervisor (Copilot SDK for periodic evaluation).
|
|
|
7
7
|
from __future__ import annotations
|
|
8
8
|
|
|
9
9
|
import asyncio
|
|
10
|
+
import datetime
|
|
10
11
|
import logging
|
|
11
12
|
import os
|
|
12
13
|
import time
|
|
@@ -66,6 +67,15 @@ class CopilotDaemon:
|
|
|
66
67
|
self._crash_times: list[float] = []
|
|
67
68
|
self._circuit_open = False
|
|
68
69
|
|
|
70
|
+
# Pause/sleep state
|
|
71
|
+
self._paused: bool = False
|
|
72
|
+
self._last_activation: float | None = None
|
|
73
|
+
|
|
74
|
+
# Status query state — for asking the agent what it's doing
|
|
75
|
+
self._status_query_event: asyncio.Event | None = None
|
|
76
|
+
self._status_query_response: str = ""
|
|
77
|
+
self._last_activity_text: str = ""
|
|
78
|
+
|
|
69
79
|
# Graceful shutdown
|
|
70
80
|
self._stop_event: asyncio.Event | None = None
|
|
71
81
|
self._pid_name: str = ""
|
|
@@ -116,18 +126,30 @@ class CopilotDaemon:
|
|
|
116
126
|
escalation_threshold=self.config.supervisor.escalation_threshold,
|
|
117
127
|
on_whisper=self._on_supervisor_whisper,
|
|
118
128
|
on_escalation=self._on_supervisor_escalation,
|
|
129
|
+
prompt_override=self.config.supervisor.prompt_override,
|
|
119
130
|
)
|
|
120
131
|
|
|
121
132
|
# 6. Optionally start the Copilot agent runner
|
|
122
133
|
if not self.skip_copilot:
|
|
123
134
|
await self._start_agent_runner()
|
|
124
135
|
|
|
136
|
+
# 7. Sleep scheduler background task
|
|
137
|
+
self._sleep_task = asyncio.create_task(self._sleep_scheduler())
|
|
138
|
+
|
|
125
139
|
logger.info(
|
|
126
140
|
"CopilotDaemon started for %s (socket=%s)", self.agent.nick, self._socket_path
|
|
127
141
|
)
|
|
128
142
|
|
|
129
143
|
async def stop(self) -> None:
|
|
130
144
|
"""Cleanly shut down all components."""
|
|
145
|
+
if hasattr(self, "_sleep_task") and self._sleep_task:
|
|
146
|
+
self._sleep_task.cancel()
|
|
147
|
+
try:
|
|
148
|
+
await self._sleep_task
|
|
149
|
+
except asyncio.CancelledError:
|
|
150
|
+
pass
|
|
151
|
+
self._sleep_task = None
|
|
152
|
+
|
|
131
153
|
if self._agent_runner is not None:
|
|
132
154
|
await self._agent_runner.stop()
|
|
133
155
|
self._agent_runner = None
|
|
@@ -146,6 +168,54 @@ class CopilotDaemon:
|
|
|
146
168
|
|
|
147
169
|
logger.info("CopilotDaemon stopped for %s", self.agent.nick)
|
|
148
170
|
|
|
171
|
+
def _parse_sleep_schedule(self) -> tuple[int, int] | None:
|
|
172
|
+
"""Parse sleep_start/sleep_end into minutes. Returns None if invalid."""
|
|
173
|
+
try:
|
|
174
|
+
sh, sm = (int(x) for x in self.config.sleep_start.split(":"))
|
|
175
|
+
wh, wm = (int(x) for x in self.config.sleep_end.split(":"))
|
|
176
|
+
if not (0 <= sh <= 23 and 0 <= sm <= 59 and 0 <= wh <= 23 and 0 <= wm <= 59):
|
|
177
|
+
raise ValueError("hours/minutes out of range")
|
|
178
|
+
return (sh * 60 + sm, wh * 60 + wm)
|
|
179
|
+
except (ValueError, AttributeError):
|
|
180
|
+
logger.warning(
|
|
181
|
+
"Invalid sleep schedule '%s'-'%s' for %s — scheduler disabled",
|
|
182
|
+
getattr(self.config, "sleep_start", None),
|
|
183
|
+
getattr(self.config, "sleep_end", None),
|
|
184
|
+
self.agent.nick,
|
|
185
|
+
)
|
|
186
|
+
return None
|
|
187
|
+
|
|
188
|
+
async def _sleep_scheduler(self) -> None:
|
|
189
|
+
"""Background task that auto-pauses/resumes based on sleep schedule."""
|
|
190
|
+
schedule = self._parse_sleep_schedule()
|
|
191
|
+
if schedule is None:
|
|
192
|
+
return
|
|
193
|
+
sleep_minutes, wake_minutes = schedule
|
|
194
|
+
|
|
195
|
+
while True:
|
|
196
|
+
try:
|
|
197
|
+
await asyncio.sleep(60) # Check every minute
|
|
198
|
+
now = datetime.datetime.now()
|
|
199
|
+
current_minutes = now.hour * 60 + now.minute
|
|
200
|
+
|
|
201
|
+
if sleep_minutes > wake_minutes:
|
|
202
|
+
# Overnight: e.g., 23:00-08:00
|
|
203
|
+
should_sleep = current_minutes >= sleep_minutes or current_minutes < wake_minutes
|
|
204
|
+
else:
|
|
205
|
+
# Same day: e.g., 13:00-14:00
|
|
206
|
+
should_sleep = sleep_minutes <= current_minutes < wake_minutes
|
|
207
|
+
|
|
208
|
+
if should_sleep and not self._paused:
|
|
209
|
+
self._paused = True
|
|
210
|
+
logger.info("Sleep schedule: pausing %s", self.agent.nick)
|
|
211
|
+
elif not should_sleep and self._paused:
|
|
212
|
+
self._paused = False
|
|
213
|
+
logger.info("Sleep schedule: resuming %s", self.agent.nick)
|
|
214
|
+
except asyncio.CancelledError:
|
|
215
|
+
return
|
|
216
|
+
except Exception:
|
|
217
|
+
logger.exception("Sleep scheduler error")
|
|
218
|
+
|
|
149
219
|
async def _graceful_shutdown(self) -> None:
|
|
150
220
|
"""Trigger a graceful shutdown, signaling any waiting stop event."""
|
|
151
221
|
logger.info("Graceful shutdown requested for %s", self.agent.nick)
|
|
@@ -186,7 +256,10 @@ class CopilotDaemon:
|
|
|
186
256
|
|
|
187
257
|
Formats a prompt and enqueues it so the Copilot session picks it up.
|
|
188
258
|
"""
|
|
259
|
+
if self._paused:
|
|
260
|
+
return
|
|
189
261
|
if self._agent_runner and self._agent_runner.is_running():
|
|
262
|
+
self._last_activation = time.time()
|
|
190
263
|
# Enqueue relay target (FIFO matches prompt queue order)
|
|
191
264
|
self._mention_targets.append(target if target.startswith("#") else sender)
|
|
192
265
|
if target.startswith("#"):
|
|
@@ -216,7 +289,24 @@ class CopilotDaemon:
|
|
|
216
289
|
if self._supervisor:
|
|
217
290
|
await self._supervisor.observe(msg)
|
|
218
291
|
|
|
292
|
+
# Capture last assistant text for status reporting
|
|
293
|
+
if msg.get("type") == "assistant":
|
|
294
|
+
for block in msg.get("content", []):
|
|
295
|
+
if isinstance(block, dict) and block.get("type") == "text":
|
|
296
|
+
self._last_activity_text = block["text"]
|
|
297
|
+
break
|
|
298
|
+
elif isinstance(block, str):
|
|
299
|
+
self._last_activity_text = block
|
|
300
|
+
break
|
|
301
|
+
|
|
302
|
+
# If a status query is pending, fulfill it
|
|
303
|
+
if self._status_query_event and not self._status_query_event.is_set():
|
|
304
|
+
self._status_query_response = self._last_activity_text
|
|
305
|
+
self._status_query_event.set()
|
|
306
|
+
|
|
219
307
|
def _build_system_prompt(self) -> str:
|
|
308
|
+
if self.agent.system_prompt:
|
|
309
|
+
return self.agent.system_prompt
|
|
220
310
|
return (
|
|
221
311
|
f"You are {self.agent.nick}, an AI agent on the agentirc IRC network.\n"
|
|
222
312
|
f"You have IRC tools available via the irc skill. Use them to communicate.\n"
|
|
@@ -340,6 +430,15 @@ class CopilotDaemon:
|
|
|
340
430
|
elif msg_type == "clear":
|
|
341
431
|
return await self._ipc_clear(req_id)
|
|
342
432
|
|
|
433
|
+
elif msg_type == "status":
|
|
434
|
+
return await self._ipc_status(req_id, msg)
|
|
435
|
+
|
|
436
|
+
elif msg_type == "pause":
|
|
437
|
+
return await self._ipc_pause(req_id)
|
|
438
|
+
|
|
439
|
+
elif msg_type == "resume":
|
|
440
|
+
return await self._ipc_resume(req_id)
|
|
441
|
+
|
|
343
442
|
elif msg_type == "shutdown":
|
|
344
443
|
asyncio.create_task(self._graceful_shutdown())
|
|
345
444
|
return make_response(req_id, ok=True)
|
|
@@ -355,6 +454,82 @@ class CopilotDaemon:
|
|
|
355
454
|
# IPC sub-handlers
|
|
356
455
|
# ------------------------------------------------------------------
|
|
357
456
|
|
|
457
|
+
async def _ipc_pause(self, req_id: str) -> dict:
|
|
458
|
+
self._paused = True
|
|
459
|
+
logger.info("Agent %s paused", self.agent.nick)
|
|
460
|
+
return make_response(req_id, ok=True)
|
|
461
|
+
|
|
462
|
+
async def _ipc_resume(self, req_id: str) -> dict:
|
|
463
|
+
self._paused = False
|
|
464
|
+
logger.info("Agent %s resumed", self.agent.nick)
|
|
465
|
+
# NOTE: Catch-up on missed messages is not yet implemented.
|
|
466
|
+
# IRCTransport does not process HISTORY responses into the buffer.
|
|
467
|
+
# The agent resumes and will see new messages going forward.
|
|
468
|
+
return make_response(req_id, ok=True)
|
|
469
|
+
|
|
470
|
+
async def _ipc_status(self, req_id: str, msg: dict | None = None) -> dict:
|
|
471
|
+
running = self._agent_runner is not None and self._agent_runner.is_running()
|
|
472
|
+
turn_count = self._supervisor._turn_count if self._supervisor else 0
|
|
473
|
+
|
|
474
|
+
# Determine activity description
|
|
475
|
+
query = msg.get("query", False) if msg else False
|
|
476
|
+
description = self._describe_activity(live_query=query)
|
|
477
|
+
|
|
478
|
+
# If live query requested and agent is active, ask the agent directly
|
|
479
|
+
if query and running and not self._paused:
|
|
480
|
+
description = await self._query_agent_status()
|
|
481
|
+
|
|
482
|
+
return make_response(req_id, ok=True, data={
|
|
483
|
+
"running": running,
|
|
484
|
+
"paused": self._paused,
|
|
485
|
+
"turn_count": turn_count,
|
|
486
|
+
"last_activation": self._last_activation,
|
|
487
|
+
"activity": "paused" if self._paused else ("working" if running else "idle"),
|
|
488
|
+
"description": description,
|
|
489
|
+
})
|
|
490
|
+
|
|
491
|
+
def _describe_activity(self, live_query: bool = False) -> str:
|
|
492
|
+
"""Return a human-readable description of what the agent is doing."""
|
|
493
|
+
if self._paused:
|
|
494
|
+
return "paused"
|
|
495
|
+
if not self._last_activity_text:
|
|
496
|
+
return "nothing"
|
|
497
|
+
# Return first line of last activity, truncated
|
|
498
|
+
first_line = self._last_activity_text.strip().split("\n")[0]
|
|
499
|
+
if len(first_line) > 120:
|
|
500
|
+
first_line = first_line[:117] + "..."
|
|
501
|
+
return first_line
|
|
502
|
+
|
|
503
|
+
async def _query_agent_status(self) -> str:
|
|
504
|
+
"""Ask the agent directly what it's working on."""
|
|
505
|
+
if not self._agent_runner or not self._agent_runner.is_running():
|
|
506
|
+
return "nothing"
|
|
507
|
+
|
|
508
|
+
self._status_query_event = asyncio.Event()
|
|
509
|
+
self._status_query_response = ""
|
|
510
|
+
|
|
511
|
+
try:
|
|
512
|
+
# Enqueue a None relay target so the status response doesn't
|
|
513
|
+
# steal a real mention's relay target from the deque.
|
|
514
|
+
self._mention_targets.append(None)
|
|
515
|
+
await self._agent_runner.send_prompt(
|
|
516
|
+
"[SYSTEM] Briefly describe what you are currently working on "
|
|
517
|
+
"in one sentence. Reply with just the description, no preamble."
|
|
518
|
+
)
|
|
519
|
+
# Wait up to 10s for the agent to respond
|
|
520
|
+
await asyncio.wait_for(self._status_query_event.wait(), timeout=10.0)
|
|
521
|
+
response = self._status_query_response.strip()
|
|
522
|
+
# Take first line, truncate
|
|
523
|
+
first_line = response.split("\n")[0]
|
|
524
|
+
if len(first_line) > 120:
|
|
525
|
+
first_line = first_line[:117] + "..."
|
|
526
|
+
return first_line or "nothing"
|
|
527
|
+
except asyncio.TimeoutError:
|
|
528
|
+
return "busy (no response)"
|
|
529
|
+
finally:
|
|
530
|
+
self._status_query_event = None
|
|
531
|
+
self._status_query_response = ""
|
|
532
|
+
|
|
358
533
|
async def _ipc_irc_send(self, req_id: str, msg: dict) -> dict:
|
|
359
534
|
channel = msg.get("channel", "")
|
|
360
535
|
text = msg.get("message", "")
|