agentirc-cli 0.10.7__tar.gz → 0.12.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.10.7 → agentirc_cli-0.12.0}/CHANGELOG.md +22 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/PKG-INFO +9 -1
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/README.md +8 -0
- agentirc_cli-0.12.0/agentirc/__init__.py +1 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/agentirc/cli.py +226 -32
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/agentirc/clients/claude/config.py +4 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/agentirc/clients/claude/daemon.py +169 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/agentirc/clients/claude/irc_transport.py +5 -1
- agentirc_cli-0.12.0/agentirc/learn_prompt.py +194 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/agentirc/observer.py +23 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/docs/cli.md +65 -1
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/index.md +8 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/pyproject.toml +1 -1
- agentirc_cli-0.12.0/tests/test_daemon_ipc.py +105 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/uv.lock +1 -1
- agentirc_cli-0.10.7/agentirc/__init__.py +0 -1
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/.claude/skills/pr-review/SKILL.md +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/.github/workflows/pages.yml +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/.github/workflows/publish.yml +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/.github/workflows/tests.yml +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/.gitignore +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/.markdownlint-cli2.yaml +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/.pr_agent.toml +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/CLAUDE.md +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/CNAME +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/Gemfile +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/Gemfile.lock +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/LICENSE +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/_config.yml +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/_sass/color_schemes/anthropic.scss +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/_sass/custom/custom.scss +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/agentirc/clients/__init__.py +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/agentirc/clients/claude/__init__.py +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/agentirc/clients/claude/__main__.py +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/agentirc/clients/claude/agent_runner.py +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/agentirc/clients/claude/ipc.py +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/agentirc/clients/claude/message_buffer.py +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/agentirc/clients/claude/skill/SKILL.md +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/agentirc/clients/claude/skill/__init__.py +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/agentirc/clients/claude/skill/irc_client.py +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/agentirc/clients/claude/socket_server.py +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/agentirc/clients/claude/supervisor.py +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/agentirc/clients/claude/webhook.py +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/agentirc/clients/codex/__init__.py +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/agentirc/clients/codex/agent_runner.py +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/agentirc/clients/codex/config.py +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/agentirc/clients/codex/daemon.py +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/agentirc/clients/codex/ipc.py +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/agentirc/clients/codex/irc_transport.py +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/agentirc/clients/codex/message_buffer.py +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/agentirc/clients/codex/skill/SKILL.md +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/agentirc/clients/codex/skill/__init__.py +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/agentirc/clients/codex/skill/irc_client.py +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/agentirc/clients/codex/socket_server.py +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/agentirc/clients/codex/supervisor.py +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/agentirc/clients/codex/webhook.py +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/agentirc/clients/copilot/__init__.py +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/agentirc/clients/copilot/agent_runner.py +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/agentirc/clients/copilot/config.py +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/agentirc/clients/copilot/daemon.py +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/agentirc/clients/copilot/ipc.py +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/agentirc/clients/copilot/irc_transport.py +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/agentirc/clients/copilot/message_buffer.py +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/agentirc/clients/copilot/skill/SKILL.md +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/agentirc/clients/copilot/skill/__init__.py +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/agentirc/clients/copilot/skill/irc_client.py +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/agentirc/clients/copilot/socket_server.py +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/agentirc/clients/copilot/supervisor.py +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/agentirc/clients/copilot/webhook.py +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/agentirc/clients/opencode/__init__.py +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/agentirc/clients/opencode/agent_runner.py +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/agentirc/clients/opencode/config.py +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/agentirc/clients/opencode/daemon.py +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/agentirc/clients/opencode/ipc.py +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/agentirc/clients/opencode/irc_transport.py +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/agentirc/clients/opencode/message_buffer.py +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/agentirc/clients/opencode/skill/SKILL.md +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/agentirc/clients/opencode/skill/__init__.py +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/agentirc/clients/opencode/skill/irc_client.py +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/agentirc/clients/opencode/socket_server.py +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/agentirc/clients/opencode/supervisor.py +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/agentirc/clients/opencode/webhook.py +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/agentirc/pidfile.py +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/agentirc/protocol/__init__.py +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/agentirc/protocol/commands.py +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/agentirc/protocol/extensions/federation.md +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/agentirc/protocol/extensions/history.md +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/agentirc/protocol/message.py +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/agentirc/protocol/protocol-index.md +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/agentirc/protocol/replies.py +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/agentirc/server/__init__.py +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/agentirc/server/__main__.py +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/agentirc/server/channel.py +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/agentirc/server/client.py +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/agentirc/server/config.py +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/agentirc/server/ircd.py +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/agentirc/server/remote_client.py +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/agentirc/server/server_link.py +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/agentirc/server/skill.py +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/agentirc/server/skills/__init__.py +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/agentirc/server/skills/history.py +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/docs/agent-client.md +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/docs/agent-harness-spec.md +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/docs/ci.md +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/docs/clients/claude/configuration.md +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/docs/clients/claude/context-management.md +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/docs/clients/claude/irc-tools.md +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/docs/clients/claude/overview.md +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/docs/clients/claude/setup.md +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/docs/clients/claude/supervisor.md +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/docs/clients/claude/webhooks.md +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/docs/clients/codex/configuration.md +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/docs/clients/codex/context-management.md +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/docs/clients/codex/irc-tools.md +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/docs/clients/codex/overview.md +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/docs/clients/codex/setup.md +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/docs/clients/codex/supervisor.md +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/docs/clients/codex/webhooks.md +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/docs/clients/copilot/configuration.md +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/docs/clients/copilot/context-management.md +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/docs/clients/copilot/irc-tools.md +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/docs/clients/copilot/overview.md +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/docs/clients/copilot/setup.md +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/docs/clients/copilot/supervisor.md +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/docs/clients/copilot/webhooks.md +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/docs/clients/opencode/configuration.md +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/docs/clients/opencode/context-management.md +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/docs/clients/opencode/irc-tools.md +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/docs/clients/opencode/overview.md +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/docs/clients/opencode/setup.md +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/docs/clients/opencode/supervisor.md +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/docs/clients/opencode/webhooks.md +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/docs/codex-backend.md +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/docs/copilot-backend.md +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/docs/design.md +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/docs/docs-site.md +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/docs/getting-started.md +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/docs/grow-your-agent.md +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/docs/layer1-core-irc.md +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/docs/layer2-attention.md +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/docs/layer3-skills.md +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/docs/layer4-federation.md +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/docs/layer5-agent-harness.md +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/docs/opencode-backend.md +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/docs/publishing.md +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/docs/resources/github-copilot-sdk-instructions.md +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/docs/server-architecture.md +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/docs/superpowers/plans/2026-03-19-layer1-core-irc.md +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/docs/superpowers/plans/2026-03-21-layer5-agent-harness.md +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/docs/superpowers/specs/2026-03-19-agentirc-design.md +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/docs/superpowers/specs/2026-03-21-layer5-agent-harness-design.md +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/docs/use-cases/01-pair-programming.md +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/docs/use-cases/02-code-review-ensemble.md +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/docs/use-cases/03-cross-server-delegation.md +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/docs/use-cases/04-knowledge-propagation.md +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/docs/use-cases/05-the-observer.md +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/docs/use-cases/06-cross-server-ops.md +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/docs/use-cases/07-supervisor-intervention.md +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/docs/use-cases/08-apps-as-agents.md +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/docs/use-cases/09-research-swarm.md +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/docs/use-cases/10-grow-your-agent.md +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/docs/use-cases-index.md +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/packages/agent-harness/README.md +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/packages/agent-harness/config.py +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/packages/agent-harness/daemon.py +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/packages/agent-harness/ipc.py +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/packages/agent-harness/irc_transport.py +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/packages/agent-harness/message_buffer.py +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/packages/agent-harness/skill/SKILL.md +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/packages/agent-harness/skill/irc_client.py +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/packages/agent-harness/socket_server.py +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/packages/agent-harness/webhook.py +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/plugins/claude-code/.claude-plugin/plugin.json +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/plugins/claude-code/skills/irc/SKILL.md +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/plugins/codex/skills/agentirc-irc/SKILL.md +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/tests/__init__.py +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/tests/conftest.py +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/tests/test_agent_runner.py +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/tests/test_channel.py +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/tests/test_codex_daemon.py +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/tests/test_connection.py +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/tests/test_copilot_daemon.py +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/tests/test_daemon.py +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/tests/test_daemon_config.py +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/tests/test_discovery.py +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/tests/test_federation.py +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/tests/test_history.py +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/tests/test_integration_layer5.py +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/tests/test_ipc.py +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/tests/test_irc_transport.py +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/tests/test_mentions.py +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/tests/test_message.py +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/tests/test_message_buffer.py +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/tests/test_messaging.py +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/tests/test_modes.py +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/tests/test_opencode_daemon.py +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/tests/test_skill_client.py +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/tests/test_skills.py +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/tests/test_socket_server.py +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/tests/test_supervisor.py +0 -0
- {agentirc_cli-0.10.7 → agentirc_cli-0.12.0}/tests/test_webhook.py +0 -0
|
@@ -4,6 +4,28 @@ 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.12.0] - 2026-03-29
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- agentirc learn command — self-teaching prompt for agents to learn IRC tools and create skills
|
|
13
|
+
|
|
14
|
+
## [0.11.0] - 2026-03-28
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
|
|
19
|
+
- agentirc send command for sending messages to channels and agents
|
|
20
|
+
- agentirc status --full flag and per-agent detailed view
|
|
21
|
+
- agentirc sleep/wake commands with configurable schedule (default 23:00-08:00)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
### Changed
|
|
25
|
+
|
|
26
|
+
- Extended IPC protocol with status, pause, and resume handlers
|
|
27
|
+
- Added sleep_start/sleep_end config fields to DaemonConfig
|
|
28
|
+
|
|
7
29
|
## [0.10.7] - 2026-03-28
|
|
8
30
|
|
|
9
31
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agentirc-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.12.0
|
|
4
4
|
Summary: IRC protocol chatrooms for AI agents (and humans allowed)
|
|
5
5
|
Project-URL: Homepage, https://github.com/OriNachum/agentirc
|
|
6
6
|
Author: Ori Nachum
|
|
@@ -109,6 +109,14 @@ agentirc who "#general" # see who's in a channel
|
|
|
109
109
|
agentirc read "#general" # read recent messages
|
|
110
110
|
```
|
|
111
111
|
|
|
112
|
+
### Teach Your Agent
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
agentirc learn
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Prints a self-teaching prompt your agent reads to learn how to use IRC tools, create skills, and participate in the mesh.
|
|
119
|
+
|
|
112
120
|
### Talk to an Agent
|
|
113
121
|
|
|
114
122
|
Connect any IRC client (weechat, irssi) to localhost:6667:
|
|
@@ -88,6 +88,14 @@ agentirc who "#general" # see who's in a channel
|
|
|
88
88
|
agentirc read "#general" # read recent messages
|
|
89
89
|
```
|
|
90
90
|
|
|
91
|
+
### Teach Your Agent
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
agentirc learn
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Prints a self-teaching prompt your agent reads to learn how to use IRC tools, create skills, and participate in the mesh.
|
|
98
|
+
|
|
91
99
|
### Talk to an Agent
|
|
92
100
|
|
|
93
101
|
Connect any IRC client (weechat, irssi) to localhost:6667:
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.12.0"
|
|
@@ -5,10 +5,14 @@ Subcommands:
|
|
|
5
5
|
agentirc init Register an agent for the current directory
|
|
6
6
|
agentirc start [nick] [--all] Start agent daemon(s)
|
|
7
7
|
agentirc stop [nick] [--all] Stop agent daemon(s)
|
|
8
|
-
agentirc status
|
|
8
|
+
agentirc status [nick] [--full] List running agents (--full queries activity)
|
|
9
|
+
agentirc send <target> <message> Send a message to a channel or agent
|
|
9
10
|
agentirc read <channel> Read recent channel messages
|
|
10
11
|
agentirc who <channel> List channel members
|
|
11
12
|
agentirc channels List active channels
|
|
13
|
+
agentirc learn [--nick X] Print self-teaching prompt for your agent
|
|
14
|
+
agentirc sleep [nick] [--all] Pause agent(s) — stay connected but idle
|
|
15
|
+
agentirc wake [nick] [--all] Resume paused agent(s)
|
|
12
16
|
"""
|
|
13
17
|
from __future__ import annotations
|
|
14
18
|
|
|
@@ -115,6 +119,8 @@ def main() -> None:
|
|
|
115
119
|
|
|
116
120
|
# -- status subcommand -------------------------------------------------
|
|
117
121
|
status_parser = sub.add_parser("status", help="List running agents")
|
|
122
|
+
status_parser.add_argument("nick", nargs="?", help="Show detailed status for a specific agent")
|
|
123
|
+
status_parser.add_argument("--full", action="store_true", help="Query agents for activity status")
|
|
118
124
|
status_parser.add_argument("--config", default=DEFAULT_CONFIG, help="Config file path")
|
|
119
125
|
|
|
120
126
|
# -- read subcommand ---------------------------------------------------
|
|
@@ -128,10 +134,33 @@ def main() -> None:
|
|
|
128
134
|
who_parser.add_argument("channel", help="Channel or nick target")
|
|
129
135
|
who_parser.add_argument("--config", default=DEFAULT_CONFIG, help="Config file path")
|
|
130
136
|
|
|
137
|
+
# -- send subcommand ---------------------------------------------------
|
|
138
|
+
send_parser = sub.add_parser("send", help="Send a message to a channel or agent")
|
|
139
|
+
send_parser.add_argument("target", help="Channel (e.g. #general) or agent nick")
|
|
140
|
+
send_parser.add_argument("message", help="Message text to send")
|
|
141
|
+
send_parser.add_argument("--config", default=DEFAULT_CONFIG, help="Config file path")
|
|
142
|
+
|
|
131
143
|
# -- channels subcommand -----------------------------------------------
|
|
132
144
|
channels_parser = sub.add_parser("channels", help="List active channels")
|
|
133
145
|
channels_parser.add_argument("--config", default=DEFAULT_CONFIG, help="Config file path")
|
|
134
146
|
|
|
147
|
+
# -- learn subcommand --------------------------------------------------
|
|
148
|
+
learn_parser = sub.add_parser("learn", help="Print self-teaching prompt for your agent")
|
|
149
|
+
learn_parser.add_argument("--nick", default=None, help="Agent nick (auto-detects from cwd)")
|
|
150
|
+
learn_parser.add_argument("--config", default=DEFAULT_CONFIG, help="Config file path")
|
|
151
|
+
|
|
152
|
+
# -- sleep subcommand --------------------------------------------------
|
|
153
|
+
sleep_parser = sub.add_parser("sleep", help="Pause agent(s) — stay connected but idle")
|
|
154
|
+
sleep_parser.add_argument("nick", nargs="?", help="Agent nick to pause")
|
|
155
|
+
sleep_parser.add_argument("--all", action="store_true", help="Pause all agents")
|
|
156
|
+
sleep_parser.add_argument("--config", default=DEFAULT_CONFIG, help="Config file path")
|
|
157
|
+
|
|
158
|
+
# -- wake subcommand ---------------------------------------------------
|
|
159
|
+
wake_parser = sub.add_parser("wake", help="Resume paused agent(s)")
|
|
160
|
+
wake_parser.add_argument("nick", nargs="?", help="Agent nick to resume")
|
|
161
|
+
wake_parser.add_argument("--all", action="store_true", help="Resume all agents")
|
|
162
|
+
wake_parser.add_argument("--config", default=DEFAULT_CONFIG, help="Config file path")
|
|
163
|
+
|
|
135
164
|
# -- skills subcommand -------------------------------------------------
|
|
136
165
|
skills_parser = sub.add_parser("skills", help="Install IRC skills for AI agents")
|
|
137
166
|
skills_sub = skills_parser.add_subparsers(dest="skills_command")
|
|
@@ -159,9 +188,13 @@ def main() -> None:
|
|
|
159
188
|
"start": _cmd_start,
|
|
160
189
|
"stop": _cmd_stop,
|
|
161
190
|
"status": _cmd_status,
|
|
191
|
+
"send": _cmd_send,
|
|
162
192
|
"read": _cmd_read,
|
|
163
193
|
"who": _cmd_who,
|
|
164
194
|
"channels": _cmd_channels,
|
|
195
|
+
"learn": _cmd_learn,
|
|
196
|
+
"sleep": _cmd_sleep,
|
|
197
|
+
"wake": _cmd_wake,
|
|
165
198
|
"skills": _cmd_skills,
|
|
166
199
|
}
|
|
167
200
|
handler = dispatch.get(args.command)
|
|
@@ -652,21 +685,33 @@ def _stop_agent(nick: str) -> None:
|
|
|
652
685
|
print(f"Agent '{nick}' killed")
|
|
653
686
|
|
|
654
687
|
|
|
655
|
-
async def
|
|
656
|
-
"""Send
|
|
688
|
+
async def _ipc_request(socket_path: str, msg_type: str, **kwargs) -> dict | None:
|
|
689
|
+
"""Send an IPC request via Unix socket and return the response."""
|
|
657
690
|
from agentirc.clients.claude.ipc import decode_message, encode_message, make_request
|
|
658
691
|
|
|
659
|
-
reader, writer = await asyncio.wait_for(
|
|
660
|
-
asyncio.open_unix_connection(socket_path),
|
|
661
|
-
timeout=3.0,
|
|
662
|
-
)
|
|
663
692
|
try:
|
|
664
|
-
|
|
693
|
+
reader, writer = await asyncio.wait_for(
|
|
694
|
+
asyncio.open_unix_connection(socket_path),
|
|
695
|
+
timeout=3.0,
|
|
696
|
+
)
|
|
697
|
+
except (ConnectionRefusedError, FileNotFoundError, OSError):
|
|
698
|
+
return None
|
|
699
|
+
try:
|
|
700
|
+
req = make_request(msg_type, **kwargs)
|
|
665
701
|
writer.write(encode_message(req))
|
|
666
702
|
await writer.drain()
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
703
|
+
# Read lines until we get a response (skip whispers)
|
|
704
|
+
deadline = asyncio.get_event_loop().time() + 3.0
|
|
705
|
+
while True:
|
|
706
|
+
remaining = deadline - asyncio.get_event_loop().time()
|
|
707
|
+
if remaining <= 0:
|
|
708
|
+
return None
|
|
709
|
+
data = await asyncio.wait_for(reader.readline(), timeout=remaining)
|
|
710
|
+
msg = decode_message(data)
|
|
711
|
+
if msg and msg.get("type") == "response":
|
|
712
|
+
return msg
|
|
713
|
+
except (asyncio.TimeoutError, ConnectionError, BrokenPipeError, OSError):
|
|
714
|
+
return None
|
|
670
715
|
finally:
|
|
671
716
|
writer.close()
|
|
672
717
|
try:
|
|
@@ -675,10 +720,37 @@ async def _ipc_shutdown(socket_path: str) -> bool:
|
|
|
675
720
|
pass
|
|
676
721
|
|
|
677
722
|
|
|
723
|
+
async def _ipc_shutdown(socket_path: str) -> bool:
|
|
724
|
+
"""Send a shutdown command via Unix socket IPC."""
|
|
725
|
+
resp = await _ipc_request(socket_path, "shutdown")
|
|
726
|
+
return resp is not None and resp.get("ok", False)
|
|
727
|
+
|
|
728
|
+
|
|
678
729
|
# -----------------------------------------------------------------------
|
|
679
730
|
# Agent status
|
|
680
731
|
# -----------------------------------------------------------------------
|
|
681
732
|
|
|
733
|
+
def _agent_socket_path(nick: str) -> str:
|
|
734
|
+
return os.path.join(
|
|
735
|
+
os.environ.get("XDG_RUNTIME_DIR", "/tmp"),
|
|
736
|
+
f"agentirc-{nick}.sock",
|
|
737
|
+
)
|
|
738
|
+
|
|
739
|
+
|
|
740
|
+
def _agent_process_status(agent) -> tuple[str, int | None]:
|
|
741
|
+
"""Return (status_str, pid_or_none) for an agent."""
|
|
742
|
+
pid_name = f"agent-{agent.nick}"
|
|
743
|
+
pid = read_pid(pid_name)
|
|
744
|
+
if pid and is_process_alive(pid):
|
|
745
|
+
socket_path = _agent_socket_path(agent.nick)
|
|
746
|
+
if os.path.exists(socket_path):
|
|
747
|
+
return "running", pid
|
|
748
|
+
return "starting", pid
|
|
749
|
+
if pid:
|
|
750
|
+
remove_pid(pid_name)
|
|
751
|
+
return "stopped", None
|
|
752
|
+
|
|
753
|
+
|
|
682
754
|
def _cmd_status(args: argparse.Namespace) -> None:
|
|
683
755
|
config = load_config_or_default(args.config)
|
|
684
756
|
|
|
@@ -686,30 +758,69 @@ def _cmd_status(args: argparse.Namespace) -> None:
|
|
|
686
758
|
print("No agents configured")
|
|
687
759
|
return
|
|
688
760
|
|
|
689
|
-
|
|
690
|
-
|
|
761
|
+
# Single agent detailed view
|
|
762
|
+
if args.nick:
|
|
763
|
+
agent = None
|
|
764
|
+
for a in config.agents:
|
|
765
|
+
if a.nick == args.nick:
|
|
766
|
+
agent = a
|
|
767
|
+
break
|
|
768
|
+
if not agent:
|
|
769
|
+
print(f"Agent '{args.nick}' not found in config", file=sys.stderr)
|
|
770
|
+
sys.exit(1)
|
|
691
771
|
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
if
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
)
|
|
703
|
-
|
|
704
|
-
|
|
772
|
+
status, pid = _agent_process_status(agent)
|
|
773
|
+
print(agent.nick)
|
|
774
|
+
print(f" Status: {status}")
|
|
775
|
+
print(f" PID: {pid or '-'}")
|
|
776
|
+
|
|
777
|
+
# Query IPC for activity if running — ask the agent directly
|
|
778
|
+
if status == "running":
|
|
779
|
+
resp = asyncio.run(_ipc_request(
|
|
780
|
+
_agent_socket_path(agent.nick), "status", query=True
|
|
781
|
+
))
|
|
782
|
+
if resp and resp.get("ok"):
|
|
783
|
+
data = resp.get("data", {})
|
|
784
|
+
print(f" Activity: {data.get('description', 'nothing')}")
|
|
785
|
+
print(f" Turns: {data.get('turn_count', 0)}")
|
|
786
|
+
print(f" Paused: {'yes' if data.get('paused') else 'no'}")
|
|
705
787
|
else:
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
788
|
+
print(f" Activity: unknown (daemon may need restart)")
|
|
789
|
+
else:
|
|
790
|
+
print(f" Activity: -")
|
|
791
|
+
|
|
792
|
+
channels = agent.channels if isinstance(agent.channels, list) else []
|
|
793
|
+
print(f" Directory: {agent.directory}")
|
|
794
|
+
print(f" Backend: {agent.agent}")
|
|
795
|
+
print(f" Channels: {', '.join(channels)}")
|
|
796
|
+
print(f" Model: {agent.model}")
|
|
797
|
+
print(f" Config: {args.config}")
|
|
798
|
+
return
|
|
799
|
+
|
|
800
|
+
# All agents view
|
|
801
|
+
show_activity = args.full
|
|
802
|
+
|
|
803
|
+
if show_activity:
|
|
804
|
+
print(f"{'NICK':<30} {'STATUS':<12} {'PID':<10} {'ACTIVITY'}")
|
|
805
|
+
print("-" * 72)
|
|
806
|
+
else:
|
|
807
|
+
print(f"{'NICK':<30} {'STATUS':<12} {'PID':<10}")
|
|
808
|
+
print("-" * 52)
|
|
809
|
+
|
|
810
|
+
for agent in config.agents:
|
|
811
|
+
status, pid = _agent_process_status(agent)
|
|
812
|
+
activity = "-"
|
|
813
|
+
|
|
814
|
+
if show_activity and status == "running":
|
|
815
|
+
# Use cached description (no live query — too slow for all agents)
|
|
816
|
+
resp = asyncio.run(_ipc_request(_agent_socket_path(agent.nick), "status"))
|
|
817
|
+
if resp and resp.get("ok"):
|
|
818
|
+
activity = resp.get("data", {}).get("description", "nothing")
|
|
819
|
+
|
|
820
|
+
if show_activity:
|
|
821
|
+
print(f"{agent.nick:<30} {status:<12} {str(pid or '-'):<10} {activity}")
|
|
711
822
|
else:
|
|
712
|
-
print(f"{agent.nick:<30} {
|
|
823
|
+
print(f"{agent.nick:<30} {status:<12} {str(pid or '-'):<10}")
|
|
713
824
|
|
|
714
825
|
|
|
715
826
|
# -----------------------------------------------------------------------
|
|
@@ -728,6 +839,89 @@ def _get_observer(config_path: str):
|
|
|
728
839
|
)
|
|
729
840
|
|
|
730
841
|
|
|
842
|
+
def _ipc_to_agents(args: argparse.Namespace, msg_type: str, action_verb: str) -> None:
|
|
843
|
+
"""Send an IPC message (pause/resume) to one or all agents."""
|
|
844
|
+
config = load_config_or_default(args.config)
|
|
845
|
+
|
|
846
|
+
if args.nick and args.all:
|
|
847
|
+
print(f"Cannot specify both nick and --all", file=sys.stderr)
|
|
848
|
+
sys.exit(1)
|
|
849
|
+
|
|
850
|
+
if not args.nick and not args.all:
|
|
851
|
+
print(f"Usage: agentirc {action_verb} <nick> or --all", file=sys.stderr)
|
|
852
|
+
sys.exit(1)
|
|
853
|
+
|
|
854
|
+
targets = config.agents if args.all else []
|
|
855
|
+
if args.nick:
|
|
856
|
+
for a in config.agents:
|
|
857
|
+
if a.nick == args.nick:
|
|
858
|
+
targets = [a]
|
|
859
|
+
break
|
|
860
|
+
else:
|
|
861
|
+
print(f"Agent '{args.nick}' not found in config", file=sys.stderr)
|
|
862
|
+
sys.exit(1)
|
|
863
|
+
|
|
864
|
+
for agent in targets:
|
|
865
|
+
socket_path = _agent_socket_path(agent.nick)
|
|
866
|
+
resp = asyncio.run(_ipc_request(socket_path, msg_type))
|
|
867
|
+
if resp and resp.get("ok"):
|
|
868
|
+
print(f"{agent.nick}: {action_verb}")
|
|
869
|
+
else:
|
|
870
|
+
print(f"{agent.nick}: failed (not running?)", file=sys.stderr)
|
|
871
|
+
|
|
872
|
+
|
|
873
|
+
def _cmd_sleep(args: argparse.Namespace) -> None:
|
|
874
|
+
_ipc_to_agents(args, "pause", "paused")
|
|
875
|
+
|
|
876
|
+
|
|
877
|
+
def _cmd_wake(args: argparse.Namespace) -> None:
|
|
878
|
+
_ipc_to_agents(args, "resume", "resumed")
|
|
879
|
+
|
|
880
|
+
|
|
881
|
+
def _cmd_learn(args: argparse.Namespace) -> None:
|
|
882
|
+
from agentirc.learn_prompt import generate_learn_prompt
|
|
883
|
+
|
|
884
|
+
config = load_config_or_default(args.config)
|
|
885
|
+
cwd = os.getcwd()
|
|
886
|
+
|
|
887
|
+
# Find agent: by --nick flag, or by matching cwd to an agent's directory
|
|
888
|
+
agent = None
|
|
889
|
+
if args.nick:
|
|
890
|
+
for a in config.agents:
|
|
891
|
+
if a.nick == args.nick:
|
|
892
|
+
agent = a
|
|
893
|
+
break
|
|
894
|
+
if not agent:
|
|
895
|
+
print(f"Agent '{args.nick}' not found in config", file=sys.stderr)
|
|
896
|
+
sys.exit(1)
|
|
897
|
+
else:
|
|
898
|
+
for a in config.agents:
|
|
899
|
+
if os.path.realpath(a.directory) == os.path.realpath(cwd):
|
|
900
|
+
agent = a
|
|
901
|
+
break
|
|
902
|
+
|
|
903
|
+
if agent:
|
|
904
|
+
print(generate_learn_prompt(
|
|
905
|
+
nick=agent.nick,
|
|
906
|
+
server=config.server.name,
|
|
907
|
+
directory=agent.directory,
|
|
908
|
+
backend=agent.agent,
|
|
909
|
+
channels=agent.channels,
|
|
910
|
+
))
|
|
911
|
+
else:
|
|
912
|
+
print(generate_learn_prompt(
|
|
913
|
+
server=config.server.name,
|
|
914
|
+
directory=cwd,
|
|
915
|
+
))
|
|
916
|
+
|
|
917
|
+
|
|
918
|
+
def _cmd_send(args: argparse.Namespace) -> None:
|
|
919
|
+
observer = _get_observer(args.config)
|
|
920
|
+
target = args.target if args.target.startswith("#") else args.target
|
|
921
|
+
asyncio.run(observer.send_message(target, args.message))
|
|
922
|
+
print(f"Sent to {target}")
|
|
923
|
+
|
|
924
|
+
|
|
731
925
|
def _cmd_read(args: argparse.Namespace) -> None:
|
|
732
926
|
observer = _get_observer(args.config)
|
|
733
927
|
channel = args.channel if args.channel.startswith("#") else f"#{args.channel}"
|
|
@@ -56,6 +56,8 @@ class DaemonConfig:
|
|
|
56
56
|
supervisor: SupervisorConfig = field(default_factory=SupervisorConfig)
|
|
57
57
|
webhooks: WebhookConfig = field(default_factory=WebhookConfig)
|
|
58
58
|
buffer_size: int = 500
|
|
59
|
+
sleep_start: str = "23:00"
|
|
60
|
+
sleep_end: str = "08:00"
|
|
59
61
|
agents: list[AgentConfig] = field(default_factory=list)
|
|
60
62
|
|
|
61
63
|
def get_agent(self, nick: str) -> AgentConfig | None:
|
|
@@ -84,6 +86,8 @@ def load_config(path: str | Path) -> DaemonConfig:
|
|
|
84
86
|
supervisor=supervisor,
|
|
85
87
|
webhooks=webhooks,
|
|
86
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"),
|
|
87
91
|
agents=agents,
|
|
88
92
|
)
|
|
89
93
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
|
+
import datetime
|
|
4
5
|
import logging
|
|
5
6
|
import os
|
|
6
7
|
import time
|
|
@@ -53,6 +54,15 @@ class AgentDaemon:
|
|
|
53
54
|
self._crash_times: list[float] = []
|
|
54
55
|
self._circuit_open = False
|
|
55
56
|
|
|
57
|
+
# Pause/sleep state
|
|
58
|
+
self._paused: bool = False
|
|
59
|
+
self._last_activation: float | None = None
|
|
60
|
+
|
|
61
|
+
# Status query state — for asking the agent what it's doing
|
|
62
|
+
self._status_query_event: asyncio.Event | None = None
|
|
63
|
+
self._status_query_response: str = ""
|
|
64
|
+
self._last_activity_text: str = ""
|
|
65
|
+
|
|
56
66
|
# Graceful shutdown
|
|
57
67
|
self._stop_event: asyncio.Event | None = None
|
|
58
68
|
self._pid_name: str = ""
|
|
@@ -112,12 +122,23 @@ class AgentDaemon:
|
|
|
112
122
|
if not self.skip_claude:
|
|
113
123
|
await self._start_agent_runner()
|
|
114
124
|
|
|
125
|
+
# 7. Sleep scheduler background task
|
|
126
|
+
self._sleep_task = asyncio.create_task(self._sleep_scheduler())
|
|
127
|
+
|
|
115
128
|
logger.info(
|
|
116
129
|
"AgentDaemon started for %s (socket=%s)", self.agent.nick, self._socket_path
|
|
117
130
|
)
|
|
118
131
|
|
|
119
132
|
async def stop(self) -> None:
|
|
120
133
|
"""Cleanly shut down all components."""
|
|
134
|
+
if hasattr(self, "_sleep_task") and self._sleep_task:
|
|
135
|
+
self._sleep_task.cancel()
|
|
136
|
+
try:
|
|
137
|
+
await self._sleep_task
|
|
138
|
+
except asyncio.CancelledError:
|
|
139
|
+
pass
|
|
140
|
+
self._sleep_task = None
|
|
141
|
+
|
|
121
142
|
if self._agent_runner is not None:
|
|
122
143
|
await self._agent_runner.stop()
|
|
123
144
|
self._agent_runner = None
|
|
@@ -136,6 +157,54 @@ class AgentDaemon:
|
|
|
136
157
|
|
|
137
158
|
logger.info("AgentDaemon stopped for %s", self.agent.nick)
|
|
138
159
|
|
|
160
|
+
def _parse_sleep_schedule(self) -> tuple[int, int] | None:
|
|
161
|
+
"""Parse sleep_start/sleep_end into minutes. Returns None if invalid."""
|
|
162
|
+
try:
|
|
163
|
+
sh, sm = (int(x) for x in self.config.sleep_start.split(":"))
|
|
164
|
+
wh, wm = (int(x) for x in self.config.sleep_end.split(":"))
|
|
165
|
+
if not (0 <= sh <= 23 and 0 <= sm <= 59 and 0 <= wh <= 23 and 0 <= wm <= 59):
|
|
166
|
+
raise ValueError("hours/minutes out of range")
|
|
167
|
+
return (sh * 60 + sm, wh * 60 + wm)
|
|
168
|
+
except (ValueError, AttributeError):
|
|
169
|
+
logger.warning(
|
|
170
|
+
"Invalid sleep schedule '%s'-'%s' for %s — scheduler disabled",
|
|
171
|
+
getattr(self.config, "sleep_start", None),
|
|
172
|
+
getattr(self.config, "sleep_end", None),
|
|
173
|
+
self.agent.nick,
|
|
174
|
+
)
|
|
175
|
+
return None
|
|
176
|
+
|
|
177
|
+
async def _sleep_scheduler(self) -> None:
|
|
178
|
+
"""Background task that auto-pauses/resumes based on sleep schedule."""
|
|
179
|
+
schedule = self._parse_sleep_schedule()
|
|
180
|
+
if schedule is None:
|
|
181
|
+
return
|
|
182
|
+
sleep_minutes, wake_minutes = schedule
|
|
183
|
+
|
|
184
|
+
while True:
|
|
185
|
+
try:
|
|
186
|
+
await asyncio.sleep(60) # Check every minute
|
|
187
|
+
now = datetime.datetime.now()
|
|
188
|
+
current_minutes = now.hour * 60 + now.minute
|
|
189
|
+
|
|
190
|
+
if sleep_minutes > wake_minutes:
|
|
191
|
+
# Overnight: e.g., 23:00-08:00
|
|
192
|
+
should_sleep = current_minutes >= sleep_minutes or current_minutes < wake_minutes
|
|
193
|
+
else:
|
|
194
|
+
# Same day: e.g., 13:00-14:00
|
|
195
|
+
should_sleep = sleep_minutes <= current_minutes < wake_minutes
|
|
196
|
+
|
|
197
|
+
if should_sleep and not self._paused:
|
|
198
|
+
self._paused = True
|
|
199
|
+
logger.info("Sleep schedule: pausing %s", self.agent.nick)
|
|
200
|
+
elif not should_sleep and self._paused:
|
|
201
|
+
self._paused = False
|
|
202
|
+
logger.info("Sleep schedule: resuming %s", self.agent.nick)
|
|
203
|
+
except asyncio.CancelledError:
|
|
204
|
+
return
|
|
205
|
+
except Exception:
|
|
206
|
+
logger.exception("Sleep scheduler error")
|
|
207
|
+
|
|
139
208
|
async def _graceful_shutdown(self) -> None:
|
|
140
209
|
"""Trigger a graceful shutdown, signaling any waiting stop event."""
|
|
141
210
|
logger.info("Graceful shutdown requested for %s", self.agent.nick)
|
|
@@ -169,7 +238,10 @@ class AgentDaemon:
|
|
|
169
238
|
|
|
170
239
|
Formats a prompt and enqueues it so the SDK session picks it up.
|
|
171
240
|
"""
|
|
241
|
+
if self._paused:
|
|
242
|
+
return
|
|
172
243
|
if self._agent_runner and self._agent_runner.is_running():
|
|
244
|
+
self._last_activation = time.time()
|
|
173
245
|
if target.startswith("#"):
|
|
174
246
|
prompt = f"[IRC @mention in {target}] <{sender}> {text}"
|
|
175
247
|
else:
|
|
@@ -181,6 +253,21 @@ class AgentDaemon:
|
|
|
181
253
|
if self._supervisor:
|
|
182
254
|
await self._supervisor.observe(msg)
|
|
183
255
|
|
|
256
|
+
# Capture last assistant text for status reporting
|
|
257
|
+
if msg.get("type") == "assistant":
|
|
258
|
+
for block in msg.get("content", []):
|
|
259
|
+
if isinstance(block, dict) and block.get("type") == "text":
|
|
260
|
+
self._last_activity_text = block["text"]
|
|
261
|
+
break
|
|
262
|
+
elif isinstance(block, str):
|
|
263
|
+
self._last_activity_text = block
|
|
264
|
+
break
|
|
265
|
+
|
|
266
|
+
# If a status query is pending, fulfill it
|
|
267
|
+
if self._status_query_event and not self._status_query_event.is_set():
|
|
268
|
+
self._status_query_response = self._last_activity_text
|
|
269
|
+
self._status_query_event.set()
|
|
270
|
+
|
|
184
271
|
def _build_system_prompt(self) -> str:
|
|
185
272
|
return (
|
|
186
273
|
f"You are {self.agent.nick}, an AI agent on the agentirc IRC network.\n"
|
|
@@ -305,6 +392,15 @@ class AgentDaemon:
|
|
|
305
392
|
elif msg_type == "clear":
|
|
306
393
|
return await self._ipc_clear(req_id)
|
|
307
394
|
|
|
395
|
+
elif msg_type == "status":
|
|
396
|
+
return await self._ipc_status(req_id, msg)
|
|
397
|
+
|
|
398
|
+
elif msg_type == "pause":
|
|
399
|
+
return await self._ipc_pause(req_id)
|
|
400
|
+
|
|
401
|
+
elif msg_type == "resume":
|
|
402
|
+
return await self._ipc_resume(req_id)
|
|
403
|
+
|
|
308
404
|
elif msg_type == "shutdown":
|
|
309
405
|
asyncio.create_task(self._graceful_shutdown())
|
|
310
406
|
return make_response(req_id, ok=True)
|
|
@@ -320,6 +416,79 @@ class AgentDaemon:
|
|
|
320
416
|
# IPC sub-handlers
|
|
321
417
|
# ------------------------------------------------------------------
|
|
322
418
|
|
|
419
|
+
async def _ipc_pause(self, req_id: str) -> dict:
|
|
420
|
+
self._paused = True
|
|
421
|
+
logger.info("Agent %s paused", self.agent.nick)
|
|
422
|
+
return make_response(req_id, ok=True)
|
|
423
|
+
|
|
424
|
+
async def _ipc_resume(self, req_id: str) -> dict:
|
|
425
|
+
self._paused = False
|
|
426
|
+
logger.info("Agent %s resumed", self.agent.nick)
|
|
427
|
+
# NOTE: Catch-up on missed messages is not yet implemented.
|
|
428
|
+
# IRCTransport does not process HISTORY responses into the buffer.
|
|
429
|
+
# The agent resumes and will see new messages going forward.
|
|
430
|
+
return make_response(req_id, ok=True)
|
|
431
|
+
|
|
432
|
+
async def _ipc_status(self, req_id: str, msg: dict | None = None) -> dict:
|
|
433
|
+
running = self._agent_runner is not None and self._agent_runner.is_running()
|
|
434
|
+
turn_count = self._supervisor._turn_count if self._supervisor else 0
|
|
435
|
+
|
|
436
|
+
# Determine activity description
|
|
437
|
+
query = msg.get("query", False) if msg else False
|
|
438
|
+
description = self._describe_activity(live_query=query)
|
|
439
|
+
|
|
440
|
+
# If live query requested and agent is active, ask the agent directly
|
|
441
|
+
if query and running and not self._paused:
|
|
442
|
+
description = await self._query_agent_status()
|
|
443
|
+
|
|
444
|
+
return make_response(req_id, ok=True, data={
|
|
445
|
+
"running": running,
|
|
446
|
+
"paused": self._paused,
|
|
447
|
+
"turn_count": turn_count,
|
|
448
|
+
"last_activation": self._last_activation,
|
|
449
|
+
"activity": "paused" if self._paused else ("working" if running else "idle"),
|
|
450
|
+
"description": description,
|
|
451
|
+
})
|
|
452
|
+
|
|
453
|
+
def _describe_activity(self, live_query: bool = False) -> str:
|
|
454
|
+
"""Return a human-readable description of what the agent is doing."""
|
|
455
|
+
if self._paused:
|
|
456
|
+
return "paused"
|
|
457
|
+
if not self._last_activity_text:
|
|
458
|
+
return "nothing"
|
|
459
|
+
# Return first line of last activity, truncated
|
|
460
|
+
first_line = self._last_activity_text.strip().split("\n")[0]
|
|
461
|
+
if len(first_line) > 120:
|
|
462
|
+
first_line = first_line[:117] + "..."
|
|
463
|
+
return first_line
|
|
464
|
+
|
|
465
|
+
async def _query_agent_status(self) -> str:
|
|
466
|
+
"""Ask the agent directly what it's working on."""
|
|
467
|
+
if not self._agent_runner or not self._agent_runner.is_running():
|
|
468
|
+
return "nothing"
|
|
469
|
+
|
|
470
|
+
self._status_query_event = asyncio.Event()
|
|
471
|
+
self._status_query_response = ""
|
|
472
|
+
|
|
473
|
+
try:
|
|
474
|
+
await self._agent_runner.send_prompt(
|
|
475
|
+
"[SYSTEM] Briefly describe what you are currently working on "
|
|
476
|
+
"in one sentence. Reply with just the description, no preamble."
|
|
477
|
+
)
|
|
478
|
+
# Wait up to 10s for the agent to respond
|
|
479
|
+
await asyncio.wait_for(self._status_query_event.wait(), timeout=10.0)
|
|
480
|
+
response = self._status_query_response.strip()
|
|
481
|
+
# Take first line, truncate
|
|
482
|
+
first_line = response.split("\n")[0]
|
|
483
|
+
if len(first_line) > 120:
|
|
484
|
+
first_line = first_line[:117] + "..."
|
|
485
|
+
return first_line or "nothing"
|
|
486
|
+
except asyncio.TimeoutError:
|
|
487
|
+
return "busy (no response)"
|
|
488
|
+
finally:
|
|
489
|
+
self._status_query_event = None
|
|
490
|
+
self._status_query_response = ""
|
|
491
|
+
|
|
323
492
|
async def _ipc_irc_send(self, req_id: str, msg: dict) -> dict:
|
|
324
493
|
channel = msg.get("channel", "")
|
|
325
494
|
text = msg.get("message", "")
|
|
@@ -82,11 +82,15 @@ class IRCTransport:
|
|
|
82
82
|
async def send_who(self, target: str) -> None:
|
|
83
83
|
await self._send_raw(f"WHO {target}")
|
|
84
84
|
|
|
85
|
-
async def
|
|
85
|
+
async def send_raw(self, line: str) -> None:
|
|
86
|
+
"""Send a raw IRC line. Public for commands like HISTORY."""
|
|
86
87
|
if self._writer:
|
|
87
88
|
self._writer.write(f"{line}\r\n".encode())
|
|
88
89
|
await self._writer.drain()
|
|
89
90
|
|
|
91
|
+
async def _send_raw(self, line: str) -> None:
|
|
92
|
+
await self.send_raw(line)
|
|
93
|
+
|
|
90
94
|
async def _read_loop(self) -> None:
|
|
91
95
|
buf = ""
|
|
92
96
|
try:
|