induscode 0.1.2__tar.gz → 0.1.4__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.
- induscode-0.1.4/CHANGELOG.md +167 -0
- {induscode-0.1.2 → induscode-0.1.4}/PKG-INFO +2 -2
- {induscode-0.1.2 → induscode-0.1.4}/pyproject.toml +4 -3
- induscode-0.1.4/src/induscode/boot/runners/delegate_runner.py +376 -0
- {induscode-0.1.2 → induscode-0.1.4}/src/induscode/boot/runners/session.py +73 -5
- {induscode-0.1.2 → induscode-0.1.4}/src/induscode/capability_deck/cards/task.py +66 -5
- {induscode-0.1.2 → induscode-0.1.4}/src/induscode/conductor/__init__.py +2 -0
- {induscode-0.1.2 → induscode-0.1.4}/src/induscode/conductor/contract.py +22 -0
- {induscode-0.1.2 → induscode-0.1.4}/src/induscode/conductor/signal_hub.py +23 -2
- {induscode-0.1.2 → induscode-0.1.4}/src/induscode/console/__init__.py +2 -0
- {induscode-0.1.2 → induscode-0.1.4}/src/induscode/console/app.py +458 -49
- {induscode-0.1.2 → induscode-0.1.4}/src/induscode/console/components/__init__.py +60 -0
- induscode-0.1.4/src/induscode/console/components/agents_view.py +388 -0
- induscode-0.1.4/src/induscode/console/components/background_agents.py +323 -0
- {induscode-0.1.2 → induscode-0.1.4}/src/induscode/console/components/banner.py +299 -47
- induscode-0.1.4/src/induscode/console/components/welcome.py +219 -0
- induscode-0.1.4/src/induscode/console/components/working_indicator.py +237 -0
- {induscode-0.1.2 → induscode-0.1.4}/src/induscode/console/contract.py +7 -3
- {induscode-0.1.2 → induscode-0.1.4}/src/induscode/console/input/chord.py +6 -3
- {induscode-0.1.2 → induscode-0.1.4}/src/induscode/console/mount.py +11 -5
- {induscode-0.1.2 → induscode-0.1.4}/src/induscode/console/overlays/__init__.py +5 -0
- induscode-0.1.4/src/induscode/console/overlays/boards.py +656 -0
- {induscode-0.1.2 → induscode-0.1.4}/src/induscode/console/overlays/pickers.py +26 -4
- {induscode-0.1.2 → induscode-0.1.4}/src/induscode/console/slash_commands/transcript.py +4 -3
- {induscode-0.1.2 → induscode-0.1.4}/src/induscode/console/slash_commands/workbench.py +2 -1
- {induscode-0.1.2 → induscode-0.1.4}/src/induscode/console/theme/__init__.py +2 -0
- {induscode-0.1.2 → induscode-0.1.4}/src/induscode/console/theme/palette.py +27 -0
- {induscode-0.1.2 → induscode-0.1.4}/src/induscode/console/theme/resolve.py +1 -0
- {induscode-0.1.2 → induscode-0.1.4}/tests/boot/test_session_persist.py +20 -1
- induscode-0.1.4/tests/conductor/test_delegate_progress.py +264 -0
- induscode-0.1.4/tests/console/test_agents_view.py +295 -0
- induscode-0.1.4/tests/console/test_background_agents.py +274 -0
- {induscode-0.1.2 → induscode-0.1.4}/tests/console/test_banner.py +115 -35
- {induscode-0.1.2 → induscode-0.1.4}/tests/console/test_commands_transcript.py +7 -6
- {induscode-0.1.2 → induscode-0.1.4}/tests/console/test_commands_workbench.py +2 -1
- {induscode-0.1.2 → induscode-0.1.4}/tests/console/test_console_app.py +241 -9
- {induscode-0.1.2 → induscode-0.1.4}/tests/console/test_e2e_pilot.py +195 -9
- {induscode-0.1.2 → induscode-0.1.4}/tests/console/test_overlays.py +223 -3
- {induscode-0.1.2 → induscode-0.1.4}/tests/console/test_reducer.py +2 -2
- {induscode-0.1.2 → induscode-0.1.4}/tests/console/test_slash_handlers.py +2 -1
- {induscode-0.1.2 → induscode-0.1.4}/tests/console/test_theme.py +49 -3
- induscode-0.1.4/tests/console/test_welcome.py +101 -0
- induscode-0.1.4/tests/console/test_working_indicator.py +187 -0
- induscode-0.1.2/CHANGELOG.md +0 -72
- {induscode-0.1.2 → induscode-0.1.4}/.gitignore +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/.pindusagi/settings.json +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/CREDITS.md +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/GAP_REPORT.md +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/NOTICE +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/PARITY_REPORT.md +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/README.md +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/scripts/lineage_scan.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/src/induscode/__init__.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/src/induscode/addons/__init__.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/src/induscode/addons/contract.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/src/induscode/addons/dispatch/__init__.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/src/induscode/addons/dispatch/event_dispatcher.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/src/induscode/addons/dispatch/tool_interceptor.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/src/induscode/addons/host.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/src/induscode/addons/loader.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/src/induscode/addons/manifest.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/src/induscode/addons/surface.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/src/induscode/boot/__init__.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/src/induscode/boot/auth_vault.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/src/induscode/boot/boot.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/src/induscode/boot/contract.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/src/induscode/boot/invocation.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/src/induscode/boot/runners/__init__.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/src/induscode/boot/runners/link_runner.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/src/induscode/boot/runners/oneshot_runner.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/src/induscode/boot/runners/registry.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/src/induscode/boot/runners/repl_runner.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/src/induscode/boot/stages.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/src/induscode/boot/upgrade/__init__.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/src/induscode/boot/upgrade/apply.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/src/induscode/boot/upgrade/upgrades.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/src/induscode/briefing/__init__.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/src/induscode/briefing/compose.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/src/induscode/briefing/contract.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/src/induscode/briefing/macros.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/src/induscode/briefing/skills.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/src/induscode/capability_deck/__init__.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/src/induscode/capability_deck/bridge_ledger/__init__.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/src/induscode/capability_deck/bridge_ledger/key.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/src/induscode/capability_deck/bridge_ledger/ledger.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/src/induscode/capability_deck/bridge_ledger/network.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/src/induscode/capability_deck/builtin_bridge.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/src/induscode/capability_deck/cards/__init__.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/src/induscode/capability_deck/cards/bg_process.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/src/induscode/capability_deck/cards/memory.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/src/induscode/capability_deck/cards/saas.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/src/induscode/capability_deck/cards/todo.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/src/induscode/capability_deck/contract.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/src/induscode/capability_deck/manifest.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/src/induscode/capability_deck/provision.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/src/induscode/channels/__init__.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/src/induscode/channels/contract.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/src/induscode/channels/framer.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/src/induscode/channels/link/__init__.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/src/induscode/channels/link/dialog.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/src/induscode/channels/link/driver.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/src/induscode/channels/link/server.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/src/induscode/channels/oneshot.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/src/induscode/channels/ops.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/src/induscode/channels/session_ops.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/src/induscode/conductor/catalog.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/src/induscode/conductor/conductor.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/src/induscode/conductor/matcher.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/src/induscode/conductor/serialize.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/src/induscode/conductor/skill_parse.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/src/induscode/conductor/transcript_store.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/src/induscode/console/components/banner_sweep.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/src/induscode/console/components/emblem.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/src/induscode/console/components/status_bar.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/src/induscode/console/input/__init__.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/src/induscode/console/input/dir_reader.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/src/induscode/console/input/intents.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/src/induscode/console/input/providers.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/src/induscode/console/overlays/auth.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/src/induscode/console/overlays/router.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/src/induscode/console/overlays/sessions.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/src/induscode/console/reducer.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/src/induscode/console/resume_picker.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/src/induscode/console/slash_commands/__init__.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/src/induscode/console/slash_commands/builtins.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/src/induscode/console/slash_commands/dynamic.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/src/induscode/console/slash_commands/integrations.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/src/induscode/console/startup.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/src/induscode/console/theme/adapter.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/src/induscode/console/theme/tokens.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/src/induscode/console_slash/__init__.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/src/induscode/console_slash/contract.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/src/induscode/console_slash/registry.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/src/induscode/console_slash/resolve.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/src/induscode/console_slash/shared.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/src/induscode/entry.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/src/induscode/insight/__init__.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/src/induscode/insight/collector.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/src/induscode/insight/replay.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/src/induscode/insight/wrapper.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/src/induscode/kit/__init__.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/src/induscode/kit/clipboard_image.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/src/induscode/kit/external_editor.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/src/induscode/kit/image.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/src/induscode/kit/shell.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/src/induscode/kit/tool_fetch.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/src/induscode/launch/__init__.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/src/induscode/launch/catalog.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/src/induscode/launch/contract.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/src/induscode/launch/credentials.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/src/induscode/launch/invocation/__init__.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/src/induscode/launch/invocation/attachments.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/src/induscode/launch/invocation/flags.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/src/induscode/launch/invocation/read.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/src/induscode/launch/invocation/usage.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/src/induscode/launch/oauth.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/src/induscode/launch/packages.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/src/induscode/launch/pickers.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/src/induscode/py.typed +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/src/induscode/runtime_bridge/__init__.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/src/induscode/runtime_bridge/bridges/__init__.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/src/induscode/runtime_bridge/bridges/_drive.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/src/induscode/runtime_bridge/bridges/builtins.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/src/induscode/runtime_bridge/bridges/claude_cli.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/src/induscode/runtime_bridge/bridges/codex_cli.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/src/induscode/runtime_bridge/bridges/indusagi_cli.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/src/induscode/runtime_bridge/broker.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/src/induscode/runtime_bridge/contract.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/src/induscode/runtime_bridge/sink.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/src/induscode/sessions/__init__.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/src/induscode/sessions/contract.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/src/induscode/sessions/library.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/src/induscode/settings/__init__.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/src/induscode/settings/contract.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/src/induscode/settings/manager.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/src/induscode/transcript_export/__init__.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/src/induscode/transcript_export/contract.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/src/induscode/transcript_export/publish.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/src/induscode/transcript_export/sgr.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/src/induscode/transcript_export/template.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/src/induscode/transcript_export/theme_bridge.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/src/induscode/window_budget/__init__.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/src/induscode/window_budget/budget/__init__.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/src/induscode/window_budget/budget/estimate.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/src/induscode/window_budget/budget/gate.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/src/induscode/window_budget/budget/slice.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/src/induscode/window_budget/condenser.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/src/induscode/window_budget/contract.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/src/induscode/window_budget/summarize/__init__.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/src/induscode/window_budget/summarize/condense.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/src/induscode/window_budget/summarize/prompt.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/src/induscode/workspace/__init__.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/src/induscode/workspace/brand.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/src/induscode/workspace/locator.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/tests/addons/test_addons.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/tests/boot/test_boot.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/tests/boot/test_invocation.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/tests/boot/test_resume_picker.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/tests/briefing/test_briefing.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/tests/capability_deck/test_cards_provision.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/tests/capability_deck/test_contract_ledger.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/tests/channels/test_channels.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/tests/conductor/test_catalog_store.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/tests/conductor/test_contract_hub_skill.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/tests/conductor/test_queue_fork.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/tests/conductor/test_submit.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/tests/console/test_commands_dynamic.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/tests/console/test_commands_integrations.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/tests/console/test_input.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/tests/console/test_startup.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/tests/console_slash/test_slash_core.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/tests/insight/test_insight.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/tests/kit/test_kit.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/tests/launch/test_launch.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/tests/launch/test_packages.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/tests/runtime_bridge/test_runtime_bridge.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/tests/sessions/test_sessions.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/tests/settings/test_settings.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/tests/test_public_api.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/tests/test_scaffold.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/tests/transcript_export/test_transcript_export.py +0 -0
- {induscode-0.1.2 → induscode-0.1.4}/tests/window_budget/test_window_budget.py +0 -0
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.4 — 2026-06-28
|
|
4
|
+
|
|
5
|
+
Interaction and reliability fixes for the live turn, the Ctrl+C / exit flow,
|
|
6
|
+
and the picker boards. Requires `indusagi >= 0.1.3` (the background-free user
|
|
7
|
+
turn). The version is single-sourced from `[project].version` via
|
|
8
|
+
`importlib.metadata`, so `--version` and the banner pick it up after a
|
|
9
|
+
reinstall.
|
|
10
|
+
|
|
11
|
+
### Live turn
|
|
12
|
+
|
|
13
|
+
- **Working spinner.** A new animated row — `⠴ Noodling… (5s · esc to
|
|
14
|
+
interrupt)` — shows while a turn is in flight: a braille spinner, a rotating
|
|
15
|
+
whimsical word (a concrete `Running tools` / `Compacting context` label
|
|
16
|
+
during those phases instead), an elapsed-seconds clock, and the interrupt
|
|
17
|
+
hint. It self-animates only while busy and hides itself when idle.
|
|
18
|
+
- **Spinner spans the whole turn.** Busy state is no longer cleared on the
|
|
19
|
+
per-round `turn_end` the framework emits between tool rounds, so the spinner
|
|
20
|
+
stays up *through* tool calls until the final answer lands — it clears only
|
|
21
|
+
at true settlement (`idle` / fault / abort).
|
|
22
|
+
|
|
23
|
+
### Pickers
|
|
24
|
+
|
|
25
|
+
- **`/model` and `/settings` boards rebuilt to match the design.** Both are now
|
|
26
|
+
hand-rolled modal screens rather than thin restyles of the framework
|
|
27
|
+
dialogs: a teal round-bordered `⌕` search box, a saffron `›` cursor on the
|
|
28
|
+
selected row, per-row value tinting, a `↓ N more below` window hint, and a
|
|
29
|
+
description / choices detail line — matching the Saffron reference boards.
|
|
30
|
+
|
|
31
|
+
### Ctrl+C, exit & abort
|
|
32
|
+
|
|
33
|
+
- **Ctrl+C no longer hard-quits on the first press.** The first Ctrl+C now
|
|
34
|
+
shows `Press Ctrl+C again to exit.` and the confirm window widened to 2s. A
|
|
35
|
+
`SIGINT` handler routes Ctrl+C that arrives as a signal (some terminals
|
|
36
|
+
deliver it that way) through the same flow, so it no longer kills the app on
|
|
37
|
+
the first press.
|
|
38
|
+
- **Silent abort.** Stopping a running turn with Ctrl+C / Esc no longer raises
|
|
39
|
+
a red `turn aborted by caller` error toast — the turn just stops quietly.
|
|
40
|
+
- **Clean exit.** The conversation transcript is no longer dumped into the
|
|
41
|
+
terminal scrollback on an interactive exit; it is emitted only when a host
|
|
42
|
+
explicitly captures it.
|
|
43
|
+
|
|
44
|
+
### Fixes
|
|
45
|
+
|
|
46
|
+
- **`/clear` (and `/new`, `/reset`) crash.** These commands dispatched raw
|
|
47
|
+
`dict` events, which the live reducer rejected with `AttributeError: 'dict'
|
|
48
|
+
object has no attribute 'type'`; they now dispatch the proper event
|
|
49
|
+
dataclasses (`RowsSet` / `BlocksClear` / `StatusClear`, and `ToggleReasoning`
|
|
50
|
+
for `/debug`).
|
|
51
|
+
|
|
52
|
+
## 0.1.3 — 2026-06-26
|
|
53
|
+
|
|
54
|
+
Saffron interface refresh and live multi-agent visibility. The console gains
|
|
55
|
+
a per-session welcome experience, a saffron theme as the default, denser
|
|
56
|
+
in-chat tooling, and a pinned view onto background agents — including
|
|
57
|
+
drill-in sub-agent progress streamed live from the task tool. No new
|
|
58
|
+
subsystem packages; the version is single-sourced from this file's
|
|
59
|
+
`[project].version` via `importlib.metadata`, so `--version` and the startup
|
|
60
|
+
banner pick it up after a reinstall (the dev-checkout fallback is unchanged).
|
|
61
|
+
|
|
62
|
+
### Theme & welcome
|
|
63
|
+
|
|
64
|
+
- **Saffron theme is the new default** — the saffron scheme registers as the
|
|
65
|
+
startup console theme, replacing the prior default.
|
|
66
|
+
- **Per-session saffron welcome card** — shown once per session, with a
|
|
67
|
+
rotating mascot, a rotating tip, and a *What's new* line summarising this
|
|
68
|
+
release.
|
|
69
|
+
- **INDUS CODE chat masthead** — a saffron masthead is stamped above the
|
|
70
|
+
transcript after the first message of a session.
|
|
71
|
+
|
|
72
|
+
### Pickers & menus
|
|
73
|
+
|
|
74
|
+
- **Saffron `/settings` board** — the settings overlay re-skinned in the
|
|
75
|
+
saffron palette.
|
|
76
|
+
- **Saffron `/model` board** — the model picker re-skinned to match.
|
|
77
|
+
- **2-column saffron slash menu** — the slash-command autocomplete renders as
|
|
78
|
+
a two-column saffron menu.
|
|
79
|
+
|
|
80
|
+
### Tool output & activity
|
|
81
|
+
|
|
82
|
+
- **Concise tool output** — tool results collapse to a 3-line preview by
|
|
83
|
+
default, expandable to ~20 lines with **Ctrl+O**.
|
|
84
|
+
- **Live Tool Activity panel disabled** — the streaming tool-activity panel is
|
|
85
|
+
turned off behind the `SHOW_TOOL_ACTIVITY` flag (default off).
|
|
86
|
+
- **Workflow tool hidden** — the workflow tool is removed from the capability
|
|
87
|
+
deck behind the `WORKFLOW_TOOL_ENABLED` flag (default off).
|
|
88
|
+
|
|
89
|
+
### Background agents
|
|
90
|
+
|
|
91
|
+
- **Pinned Background Agents panel** — a saffron panel pinned in the console
|
|
92
|
+
listing active background agents.
|
|
93
|
+
- **Subagent drill-in view** — press **←** on a background agent to open a
|
|
94
|
+
dedicated Subagent View.
|
|
95
|
+
- **Live sub-agent progress** — sub-agent token counts and activity stream
|
|
96
|
+
live from the task tool into the panel and drill-in view.
|
|
97
|
+
|
|
98
|
+
## 0.1.0 — 2026-06-11
|
|
99
|
+
|
|
100
|
+
First release of the Python rebuild. Full port of the TypeScript
|
|
101
|
+
`indusagi-coding-agent` (lineage **v0.1.62**, `indus-code-rebuild`) onto the
|
|
102
|
+
Python `indusagi` framework, built milestone-by-milestone (M0–M6) per
|
|
103
|
+
`indus-code-rebuild/PYTHON_PORT_PLAN/PLAN.md`. **649 tests green**, lineage
|
|
104
|
+
scan clean, wheel verified in a fresh venv.
|
|
105
|
+
|
|
106
|
+
### Subsystems
|
|
107
|
+
|
|
108
|
+
- **workspace** — brand/version (single-sourced via `importlib.metadata`,
|
|
109
|
+
literal fallback) and the `~/.pindusagi` locator composed over the
|
|
110
|
+
framework `Locator` (`INDUSAGI_CODING_AGENT_DIR` > `INDUSAGI_HOME`).
|
|
111
|
+
- **kit** — leaf utilities.
|
|
112
|
+
- **settings** — two-tier `PreferenceStore` (project-write-only).
|
|
113
|
+
- **briefing** — system-prompt composition, macro scanner, SKILL.md walker.
|
|
114
|
+
- **window_budget** — context-window budgeting and condense seam
|
|
115
|
+
(3.6 chars/token heuristic kept).
|
|
116
|
+
- **insight** — tracing wrapper over `indusagi.tracing` with collector sink,
|
|
117
|
+
replay, and FNV-1a gate.
|
|
118
|
+
- **transcript_export** — SGR state machine, theme bridge, HTML publisher on
|
|
119
|
+
`markdown-it-py` + `pygments` (replacing `marked` + `highlight.js`).
|
|
120
|
+
- **conductor** — turn loop with retry/queue-drain/persist-tail, condense
|
|
121
|
+
seam, fork/navigate, signal hub (10-tag translator table), model
|
|
122
|
+
catalog/matcher over `indusagi.ai`, branchable NDJSON `TranscriptStore`,
|
|
123
|
+
message⇄dict codec (`serialize.py`), skill parsing.
|
|
124
|
+
- **runtime_bridge** — snapshot sink (mutable builder, per-push frozen
|
|
125
|
+
snapshots), broker (route/exchange/resume-tap), exchange driver + dialect
|
|
126
|
+
parsers.
|
|
127
|
+
- **capability_deck** — builtin-bridge factory table, `PROFILE_TABLE`
|
|
128
|
+
provisioning, cards (todo ledger, bg-process daemon table on asyncio
|
|
129
|
+
subprocesses with SIGTERM→SIGKILL grace, task/saas/memory), bridge ledger
|
|
130
|
+
with ULID keys (`python-ulid`) and `mount_protocol_bridge` networking.
|
|
131
|
+
- **addons** — discovery walk, importlib loader, event dispatcher
|
|
132
|
+
(gate-throw fails open, transform-throw keeps prior payload), host with
|
|
133
|
+
reserved names and first-wins conflicts.
|
|
134
|
+
- **launch / boot / channels / sessions** — 17-flag invocation table on the
|
|
135
|
+
extended framework parser (`@file` attachments, `--`, list flags, `-pi`
|
|
136
|
+
clustering), credentials + OAuth adapter, multi-account `AuthVault` as the
|
|
137
|
+
single `auth.json` writer (0600, tolerant of framework single-record
|
|
138
|
+
shape), boot orchestrator + runners (oneshot/link/repl), NDJSON channel
|
|
139
|
+
framer (U+2028/29 escapes), session ops + link server + dialog bridge,
|
|
140
|
+
`SessionLibrary` over the transcript store.
|
|
141
|
+
- **console / console_slash** — Textual TUI replacing the Ink console:
|
|
142
|
+
theme engine (4 schemes as native Textual themes), reducer, chrome
|
|
143
|
+
widgets, editor/keybindings with chord latch, autocomplete, overlays as
|
|
144
|
+
`push_screen` flows (incl. OAuth sign-in on `asyncio.Future`), and **26
|
|
145
|
+
slash commands** (18 transcript/workbench built-ins + 8 integration
|
|
146
|
+
commands, plus dynamic skill/template commands).
|
|
147
|
+
- **entry** — console scripts `pindus` and `induscode`.
|
|
148
|
+
|
|
149
|
+
### Key decisions
|
|
150
|
+
|
|
151
|
+
- **Clean-room discipline** — independent implementation; no source copied
|
|
152
|
+
from any prior codebase; enforced by `scripts/lineage_scan.py` and the
|
|
153
|
+
public-API guard test, both release gates.
|
|
154
|
+
- **Framework reuse over reinvention** — LLM abstraction, agent core, MCP
|
|
155
|
+
client, Textual shell primitives, and stage runner come from `indusagi`
|
|
156
|
+
(`[mcp,tui]` extras required); the agent keeps only its own seams
|
|
157
|
+
(transcript tree, vault, signal translation, deck).
|
|
158
|
+
- **Behavioral parity with the TS lineage** — suites ported at full
|
|
159
|
+
strength (649 pytest cases vs the TS line's 332 vitest cases, never
|
|
160
|
+
weakened); versioning restarts at 0.1.0 while the TS line was at 0.1.62.
|
|
161
|
+
- **Library swaps** — `marked`/`highlight.js` → `markdown-it-py`/`pygments`;
|
|
162
|
+
`ulid` → `python-ulid`; Ink/React → Textual; jiti virtual modules →
|
|
163
|
+
plain `importlib` addon loading.
|
|
164
|
+
- **Packaging** — hatchling build; `py.typed` shipped; `NOTICE` and
|
|
165
|
+
`CREDITS.md` embedded via PEP 639 `license-files`; wheel gate = fresh
|
|
166
|
+
venv install (framework wheel first) + `--version`/`--help`/
|
|
167
|
+
`--list-models`/subsystem-import smokes.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: induscode
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.4
|
|
4
4
|
Summary: Indusagi coding agent — terminal-first AI coding agent on the indusagi framework (Python rebuild)
|
|
5
5
|
Project-URL: Homepage, https://github.com/varunisrani/indusagi-ts
|
|
6
6
|
Project-URL: Repository, https://github.com/varunisrani/indusagi-ts
|
|
@@ -22,7 +22,7 @@ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
|
22
22
|
Classifier: Topic :: Software Development :: Code Generators
|
|
23
23
|
Classifier: Typing :: Typed
|
|
24
24
|
Requires-Python: >=3.11
|
|
25
|
-
Requires-Dist: indusagi[mcp,tui]>=0.1.
|
|
25
|
+
Requires-Dist: indusagi[mcp,tui]>=0.1.3
|
|
26
26
|
Requires-Dist: markdown-it-py>=3.0
|
|
27
27
|
Requires-Dist: pygments>=2.18
|
|
28
28
|
Requires-Dist: python-ulid>=2.7
|
|
@@ -8,7 +8,7 @@ name = "induscode"
|
|
|
8
8
|
# it ports the TypeScript lineage `indusagi-coding-agent`, which was at VERSION
|
|
9
9
|
# 0.1.62 (indus-code-rebuild/src/workspace/brand.ts) when this port began.
|
|
10
10
|
# Runtime code reads this via importlib.metadata — never duplicate it.
|
|
11
|
-
version = "0.1.
|
|
11
|
+
version = "0.1.4"
|
|
12
12
|
description = "Indusagi coding agent — terminal-first AI coding agent on the indusagi framework (Python rebuild)"
|
|
13
13
|
authors = [{ name = "Varun Israni" }, { name = "IndusAGI Team", email = "team@indusagi.ai" }]
|
|
14
14
|
readme = "README.md"
|
|
@@ -32,10 +32,11 @@ dependencies = [
|
|
|
32
32
|
# The framework, from PyPI. The [mcp,tui] extras are required: the framework's
|
|
33
33
|
# shell_app barrel (which our workspace layer composes) imports `mcp` and
|
|
34
34
|
# `textual` eagerly, and the agent's own MCP enrollment + Textual console need
|
|
35
|
-
# them anyway. Pinned to the published framework (>=0.1.
|
|
35
|
+
# them anyway. Pinned to the published framework (>=0.1.3 — the
|
|
36
|
+
# background-free user turn lands in the framework's react_ink).
|
|
36
37
|
# For local dev, editable-install the framework first:
|
|
37
38
|
# pip install -e ../indusagi-python-rebuild (then pip install -e .)
|
|
38
|
-
"indusagi[mcp,tui]>=0.1.
|
|
39
|
+
"indusagi[mcp,tui]>=0.1.3",
|
|
39
40
|
"markdown-it-py>=3.0", # transcript-export markdown (replaces marked)
|
|
40
41
|
"pygments>=2.18", # transcript-export highlighting (replaces highlight.js)
|
|
41
42
|
"python-ulid>=2.7", # bridge-ledger ULID keys (capability_deck)
|
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
"""Boot helper: a live :class:`DelegateRunner` that makes the ``task`` tool real.
|
|
2
|
+
|
|
3
|
+
Port of TS ``src/boot/runners/delegate-runner.ts``.
|
|
4
|
+
|
|
5
|
+
The ``task`` capability (:mod:`induscode.capability_deck.cards.task`) advertises
|
|
6
|
+
sub-agent delegation but only *runs* it when a :class:`DelegateRunner` is wired
|
|
7
|
+
into the deck context under :data:`DELEGATE_HANDLE_KEY`; absent one it degrades
|
|
8
|
+
to a typed stub. This module supplies that runner.
|
|
9
|
+
|
|
10
|
+
Each delegated objective spins a *fresh, isolated* framework :class:`~indusagi.agent.Agent`
|
|
11
|
+
(the same ``Agent`` the conductor already drives) bound to the parent's model id
|
|
12
|
+
and credential resolver, given a survey-style system prompt and a deck that
|
|
13
|
+
DELIBERATELY excludes the ``task`` card. That exclusion is the recursion guard:
|
|
14
|
+
a sub-agent with no ``task`` tool cannot spawn its own sub-agents. The runner
|
|
15
|
+
submits one prompt, lets the sub-agent run its own tool loop to completion, then
|
|
16
|
+
extracts the final assistant turn's text as the single report handed back to the
|
|
17
|
+
parent.
|
|
18
|
+
|
|
19
|
+
The runner never raises out of :meth:`run`: a model that does not resolve, a
|
|
20
|
+
sub-agent error, or an empty transcript all map to a ``DelegateResult(ok=False,
|
|
21
|
+
report=...)`` so a delegation failure surfaces to the parent agent as an
|
|
22
|
+
ordinary tool error rather than crashing the turn.
|
|
23
|
+
|
|
24
|
+
The ``spawn`` / ``tools`` options are pure test seams — they let a unit test
|
|
25
|
+
drive the runner with an in-memory fake instead of a real network round-trip.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
from collections.abc import Awaitable, Callable, Sequence
|
|
31
|
+
from typing import Any, Protocol
|
|
32
|
+
|
|
33
|
+
from induscode.capability_deck import AgentTool, DeckContext, provision_deck
|
|
34
|
+
from induscode.conductor import ModelCatalog, ModelMatcher
|
|
35
|
+
|
|
36
|
+
from ...capability_deck.cards.task import (
|
|
37
|
+
ActivityLine,
|
|
38
|
+
DelegateProgress,
|
|
39
|
+
DelegateRequest,
|
|
40
|
+
DelegateResult,
|
|
41
|
+
DelegateRunner,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
__all__ = [
|
|
45
|
+
"DelegateRunnerOptions",
|
|
46
|
+
"DelegateSubAgent",
|
|
47
|
+
"create_delegate_runner",
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
# ---------------------------------------------------------------------------
|
|
52
|
+
# The minimal sub-agent surface the runner drives
|
|
53
|
+
# ---------------------------------------------------------------------------
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class DelegateSubAgent(Protocol):
|
|
57
|
+
"""The minimal sub-agent surface the runner drives.
|
|
58
|
+
|
|
59
|
+
A real framework :class:`~indusagi.agent.Agent` satisfies this
|
|
60
|
+
structurally; a test passes a lightweight fake via
|
|
61
|
+
:attr:`DelegateRunnerOptions.spawn`. Only the pieces the runner touches are
|
|
62
|
+
named — submit a prompt, abort, and read the resulting transcript/error
|
|
63
|
+
afterward.
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
async def prompt(self, input: str) -> None: ...
|
|
67
|
+
|
|
68
|
+
def abort(self) -> None: ...
|
|
69
|
+
|
|
70
|
+
@property
|
|
71
|
+
def state(self) -> Any:
|
|
72
|
+
"""The agent state: ``.messages`` (a sequence) and an optional
|
|
73
|
+
``.error`` string."""
|
|
74
|
+
...
|
|
75
|
+
|
|
76
|
+
# Optional event subscription (the real framework Agent provides it). The
|
|
77
|
+
# runner uses it to recompute live token spend as the sub-agent works; a
|
|
78
|
+
# test fake may omit it, in which case no progress is reported. (A Python
|
|
79
|
+
# Protocol cannot mark a method optional, so the runner probes with
|
|
80
|
+
# ``getattr`` instead of declaring it here.)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
# ---------------------------------------------------------------------------
|
|
84
|
+
# Transcript projection helpers
|
|
85
|
+
# ---------------------------------------------------------------------------
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _field(obj: Any, name: str, default: Any = None) -> Any:
|
|
89
|
+
"""Read ``name`` off a mapping key or an attribute, whichever the value
|
|
90
|
+
carries (framework messages are dataclasses; tests may pass dicts)."""
|
|
91
|
+
if isinstance(obj, dict):
|
|
92
|
+
return obj.get(name, default)
|
|
93
|
+
return getattr(obj, name, default)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _sum_tokens(messages: Sequence[Any]) -> int:
|
|
97
|
+
"""Sum the token spend across a sub-agent's assistant turns.
|
|
98
|
+
|
|
99
|
+
Each assistant message carries a ``usage`` of input/output token counts;
|
|
100
|
+
this totals input + output across the transcript so far, tolerating
|
|
101
|
+
messages that carry no usage (user/tool turns, or a turn still streaming).
|
|
102
|
+
"""
|
|
103
|
+
total = 0
|
|
104
|
+
for message in messages:
|
|
105
|
+
usage = _field(message, "usage")
|
|
106
|
+
if usage is not None:
|
|
107
|
+
total += int(_field(usage, "input", 0) or 0)
|
|
108
|
+
total += int(_field(usage, "output", 0) or 0)
|
|
109
|
+
return total
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _one_line(text: str, max_len: int = 120) -> str:
|
|
113
|
+
"""Collapse text to a single trimmed line clipped to ``max_len`` chars."""
|
|
114
|
+
flat = " ".join(text.split()).strip()
|
|
115
|
+
if len(flat) > max_len:
|
|
116
|
+
return f"{flat[: max_len - 1]}…"
|
|
117
|
+
return flat
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _arg_preview(args: Any) -> str:
|
|
121
|
+
"""A compact ``(arg)`` preview from a tool call's first argument value."""
|
|
122
|
+
if not isinstance(args, dict) or len(args) == 0:
|
|
123
|
+
return ""
|
|
124
|
+
value = next(iter(args.values()))
|
|
125
|
+
if isinstance(value, str):
|
|
126
|
+
text = value
|
|
127
|
+
else:
|
|
128
|
+
import json
|
|
129
|
+
|
|
130
|
+
try:
|
|
131
|
+
text = json.dumps(value)
|
|
132
|
+
except Exception:
|
|
133
|
+
text = str(value)
|
|
134
|
+
return f" ({_one_line(str(text), 56)})"
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _build_activity(messages: Sequence[Any]) -> tuple[ActivityLine, ...]:
|
|
138
|
+
"""Project a sub-agent's recent activity into compact display lines.
|
|
139
|
+
|
|
140
|
+
Splits assistant messages into reasoning/answer snippets and tool calls,
|
|
141
|
+
newest last, capped at the last ~14 so the stream stays light to ship over
|
|
142
|
+
the progress channel.
|
|
143
|
+
"""
|
|
144
|
+
out: list[ActivityLine] = []
|
|
145
|
+
for message in messages:
|
|
146
|
+
if _field(message, "role") != "assistant":
|
|
147
|
+
continue
|
|
148
|
+
content = _field(message, "content")
|
|
149
|
+
if not isinstance(content, (list, tuple)):
|
|
150
|
+
continue
|
|
151
|
+
for part in content:
|
|
152
|
+
part_type = _field(part, "type")
|
|
153
|
+
if part_type == "text":
|
|
154
|
+
text = _field(part, "text")
|
|
155
|
+
if isinstance(text, str) and text.strip():
|
|
156
|
+
out.append(ActivityLine(tone="reason", text=_one_line(text)))
|
|
157
|
+
elif part_type == "thinking":
|
|
158
|
+
thinking = _field(part, "thinking")
|
|
159
|
+
if isinstance(thinking, str) and thinking.strip():
|
|
160
|
+
out.append(ActivityLine(tone="reason", text=_one_line(thinking)))
|
|
161
|
+
elif part_type == "toolCall":
|
|
162
|
+
name = _field(part, "name")
|
|
163
|
+
if isinstance(name, str):
|
|
164
|
+
label = name[:1].upper() + name[1:]
|
|
165
|
+
out.append(
|
|
166
|
+
ActivityLine(
|
|
167
|
+
tone="tool",
|
|
168
|
+
text=f"{label}{_arg_preview(_field(part, 'arguments'))}",
|
|
169
|
+
)
|
|
170
|
+
)
|
|
171
|
+
return tuple(out[-14:])
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _final_assistant_text(messages: Sequence[Any]) -> str:
|
|
175
|
+
"""Extract the report text from a finished sub-agent transcript.
|
|
176
|
+
|
|
177
|
+
Walks backward to the last ``assistant`` message and joins its
|
|
178
|
+
``type=='text'`` blocks; thinking and tool-call blocks carry no report and
|
|
179
|
+
are skipped. Returns ``''`` when there is no assistant turn (or it produced
|
|
180
|
+
no text), which the caller maps to a ``'(no output)'`` report.
|
|
181
|
+
"""
|
|
182
|
+
for message in reversed(list(messages)):
|
|
183
|
+
if _field(message, "role") != "assistant":
|
|
184
|
+
continue
|
|
185
|
+
content = _field(message, "content")
|
|
186
|
+
if not isinstance(content, (list, tuple)):
|
|
187
|
+
return content if isinstance(content, str) else ""
|
|
188
|
+
parts: list[str] = []
|
|
189
|
+
for block in content:
|
|
190
|
+
if _field(block, "type") == "text":
|
|
191
|
+
text = _field(block, "text")
|
|
192
|
+
if isinstance(text, str):
|
|
193
|
+
parts.append(text)
|
|
194
|
+
return "".join(parts)
|
|
195
|
+
return ""
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
# ---------------------------------------------------------------------------
|
|
199
|
+
# Options
|
|
200
|
+
# ---------------------------------------------------------------------------
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
class DelegateRunnerOptions:
|
|
204
|
+
"""Configuration for :func:`create_delegate_runner`.
|
|
205
|
+
|
|
206
|
+
A plain options bag (the TS interface). ``spawn`` and ``tools`` are pure
|
|
207
|
+
test seams.
|
|
208
|
+
"""
|
|
209
|
+
|
|
210
|
+
def __init__(
|
|
211
|
+
self,
|
|
212
|
+
*,
|
|
213
|
+
modelId: str,
|
|
214
|
+
cwd: str,
|
|
215
|
+
system: str,
|
|
216
|
+
getApiKey: Callable[[str], Awaitable[str | None] | str | None] | None = None,
|
|
217
|
+
spawn: Callable[[str, str | None], DelegateSubAgent] | None = None,
|
|
218
|
+
tools: Callable[[], list[AgentTool]] | None = None,
|
|
219
|
+
) -> None:
|
|
220
|
+
#: The model id the sub-agent runs under (the parent's resolved model).
|
|
221
|
+
self.modelId = modelId
|
|
222
|
+
#: The working directory the sub-agent's deck is scoped to.
|
|
223
|
+
self.cwd = cwd
|
|
224
|
+
#: The system prompt that shapes the sub-agent's behaviour.
|
|
225
|
+
self.system = system
|
|
226
|
+
#: Per-call credential resolver, forwarded to the framework Agent.
|
|
227
|
+
self.getApiKey = getApiKey
|
|
228
|
+
#: Test seam: build the sub-agent from the objective.
|
|
229
|
+
self.spawn = spawn
|
|
230
|
+
#: Test seam: the tool deck the sub-agent runs with.
|
|
231
|
+
self.tools = tools
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
# ---------------------------------------------------------------------------
|
|
235
|
+
# Runner factory
|
|
236
|
+
# ---------------------------------------------------------------------------
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
class _LiveDelegateRunner:
|
|
240
|
+
"""A live :class:`DelegateRunner` bound to a resolved model + options."""
|
|
241
|
+
|
|
242
|
+
def __init__(self, options: DelegateRunnerOptions, model: Any) -> None:
|
|
243
|
+
self._options = options
|
|
244
|
+
# Resolved once up front; ``None`` means every run reports ok=False
|
|
245
|
+
# rather than raising (a misconfigured model can never crash a turn).
|
|
246
|
+
self._model = model
|
|
247
|
+
|
|
248
|
+
def _spawn_agent(self, request: DelegateRequest) -> DelegateSubAgent:
|
|
249
|
+
from indusagi.agent import Agent
|
|
250
|
+
|
|
251
|
+
opts = self._options
|
|
252
|
+
if opts.tools is not None:
|
|
253
|
+
tools = opts.tools()
|
|
254
|
+
else:
|
|
255
|
+
# The sub-agent's deck — the 'authoring' profile excludes the
|
|
256
|
+
# ``task`` card, so the sub-agent cannot recurse into delegation.
|
|
257
|
+
tools = provision_deck("authoring", DeckContext(cwd=opts.cwd)).tools()
|
|
258
|
+
initial_state: dict[str, Any] = {
|
|
259
|
+
# ``model`` is guaranteed non-None here: the run() guard returns
|
|
260
|
+
# early when no spawn seam was given and the model did not resolve.
|
|
261
|
+
"model": self._model,
|
|
262
|
+
"systemPrompt": opts.system,
|
|
263
|
+
"tools": list(tools),
|
|
264
|
+
}
|
|
265
|
+
kwargs: dict[str, Any] = {}
|
|
266
|
+
if opts.getApiKey is not None:
|
|
267
|
+
kwargs["get_api_key"] = opts.getApiKey
|
|
268
|
+
return Agent(initial_state=initial_state, **kwargs) # type: ignore[return-value]
|
|
269
|
+
|
|
270
|
+
async def run(
|
|
271
|
+
self,
|
|
272
|
+
request: DelegateRequest,
|
|
273
|
+
signal: object | None = None,
|
|
274
|
+
on_progress: Callable[[DelegateProgress], None] | None = None,
|
|
275
|
+
) -> DelegateResult:
|
|
276
|
+
opts = self._options
|
|
277
|
+
if opts.spawn is None and self._model is None:
|
|
278
|
+
return DelegateResult(ok=False, report="no model resolved for delegation")
|
|
279
|
+
if signal is not None and bool(getattr(signal, "aborted", False)):
|
|
280
|
+
return DelegateResult(ok=False, report="delegation aborted")
|
|
281
|
+
|
|
282
|
+
# Fold any parent-supplied context into the objective so the otherwise
|
|
283
|
+
# history-less sub-agent starts with everything it needs.
|
|
284
|
+
objective = (
|
|
285
|
+
f"{request.objective}\n\nContext:\n{request.context}"
|
|
286
|
+
if request.context
|
|
287
|
+
else request.objective
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
agent: DelegateSubAgent = (
|
|
291
|
+
opts.spawn(request.objective, request.context)
|
|
292
|
+
if opts.spawn is not None
|
|
293
|
+
else self._spawn_agent(request)
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
# Forward cancellation: an abort raised mid-run calls agent.abort().
|
|
297
|
+
on_abort = agent.abort
|
|
298
|
+
add_listener = getattr(signal, "add_event_listener", None) or getattr(
|
|
299
|
+
signal, "addEventListener", None
|
|
300
|
+
)
|
|
301
|
+
if add_listener is not None:
|
|
302
|
+
try:
|
|
303
|
+
add_listener("abort", on_abort)
|
|
304
|
+
except Exception:
|
|
305
|
+
add_listener = None
|
|
306
|
+
|
|
307
|
+
# Stream live token spend while the sub-agent works: recompute the
|
|
308
|
+
# running total on every agent event and report it (only when it
|
|
309
|
+
# changes) so the host's background-agents panel shows a live count. The
|
|
310
|
+
# framework Agent provides ``subscribe``; a fake without it reports
|
|
311
|
+
# nothing.
|
|
312
|
+
last_signature = ""
|
|
313
|
+
|
|
314
|
+
def report_progress() -> None:
|
|
315
|
+
nonlocal last_signature
|
|
316
|
+
if on_progress is None:
|
|
317
|
+
return
|
|
318
|
+
messages = _field(agent.state, "messages") or ()
|
|
319
|
+
tokens = _sum_tokens(messages)
|
|
320
|
+
activity = _build_activity(messages)
|
|
321
|
+
signature = f"{tokens}:{len(activity)}"
|
|
322
|
+
if signature != last_signature:
|
|
323
|
+
last_signature = signature
|
|
324
|
+
on_progress(DelegateProgress(tokens=tokens, activity=activity))
|
|
325
|
+
|
|
326
|
+
off_progress: Callable[[], None] | None = None
|
|
327
|
+
subscribe = getattr(agent, "subscribe", None)
|
|
328
|
+
if on_progress is not None and callable(subscribe):
|
|
329
|
+
off_progress = subscribe(lambda _event: report_progress())
|
|
330
|
+
|
|
331
|
+
try:
|
|
332
|
+
await agent.prompt(objective)
|
|
333
|
+
report_progress()
|
|
334
|
+
except Exception as cause: # noqa: BLE001 — a sub-agent failure is a
|
|
335
|
+
# delegation failure, not a crash of the parent's tool call.
|
|
336
|
+
message = str(cause)
|
|
337
|
+
return DelegateResult(ok=False, report=message or "delegation failed")
|
|
338
|
+
finally:
|
|
339
|
+
if off_progress is not None:
|
|
340
|
+
try:
|
|
341
|
+
off_progress()
|
|
342
|
+
except Exception:
|
|
343
|
+
pass
|
|
344
|
+
remove_listener = getattr(signal, "remove_event_listener", None) or getattr(
|
|
345
|
+
signal, "removeEventListener", None
|
|
346
|
+
)
|
|
347
|
+
if add_listener is not None and remove_listener is not None:
|
|
348
|
+
try:
|
|
349
|
+
remove_listener("abort", on_abort)
|
|
350
|
+
except Exception:
|
|
351
|
+
pass
|
|
352
|
+
|
|
353
|
+
error = _field(agent.state, "error")
|
|
354
|
+
report = _final_assistant_text(_field(agent.state, "messages") or ())
|
|
355
|
+
return DelegateResult(
|
|
356
|
+
ok=error is None and len(report) > 0,
|
|
357
|
+
report=error if error is not None else (report or "(no output)"),
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def create_delegate_runner(opts: DelegateRunnerOptions) -> DelegateRunner:
|
|
362
|
+
"""Build a live :class:`DelegateRunner` the host wires into the deck context.
|
|
363
|
+
|
|
364
|
+
The model is resolved once up front; if the id resolves to nothing the
|
|
365
|
+
runner still builds but every :meth:`run` reports ``ok=False`` rather than
|
|
366
|
+
raising, so a misconfigured model can never crash the parent's turn.
|
|
367
|
+
|
|
368
|
+
:param opts: the model id, cwd, system prompt, and optional credential/test
|
|
369
|
+
seams
|
|
370
|
+
:returns: a runner satisfying the task card's :class:`DelegateRunner`
|
|
371
|
+
contract
|
|
372
|
+
"""
|
|
373
|
+
# Resolve the framework Model once (same pattern the conductor uses).
|
|
374
|
+
card = ModelMatcher(ModelCatalog()).resolve_card(opts.modelId)
|
|
375
|
+
model = card.model if card is not None else None
|
|
376
|
+
return _LiveDelegateRunner(opts, model)
|